IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

docker-client.ts

Buka raw
import "server-only";

import { request } from "node:http";
import { serverEnv } from "@/lib/server/env";
import type { DockerPullProgressDetail } from "@/lib/shared/contracts/apps";

type DockerProgressDetailRaw = {
  current?: unknown;
  total?: unknown;
};

type DockerPullEventRaw = {
  status?: unknown;
  id?: unknown;
  progress?: unknown;
  progressDetail?: DockerProgressDetailRaw;
  error?: unknown;
};

export type DockerPullEvent = {
  status: string;
  id?: string;
  progress?: string;
  progressDetail?: DockerPullProgressDetail;
  error?: string;
};

function toNumber(value: unknown) {
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
}

function toStringOrUndefined(value: unknown) {
  return typeof value === "string" ? value : undefined;
}

export function normalizeDockerProgressDetail(
  value: DockerProgressDetailRaw | undefined,
): DockerPullProgressDetail | undefined {
  if (!value) return undefined;

  const current = toNumber(value.current);
  const total = toNumber(value.total);
  const percent = total > 0 ? Number(((current / total) * 100).toFixed(2)) : null;

  return {
    current,
    total,
    percent,
  };
}

export function parseDockerPullEvent(value: unknown): DockerPullEvent | null {
  if (!value || typeof value !== "object") {
    return null;
  }

  const raw = value as DockerPullEventRaw;
  const status = toStringOrUndefined(raw.status) ?? "unknown";
  const parsed: DockerPullEvent = {
    status,
  };

  const id = toStringOrUndefined(raw.id);
  if (id) parsed.id = id;

  const progress = toStringOrUndefined(raw.progress);
  if (progress) parsed.progress = progress;

  const progressDetail = normalizeDockerProgressDetail(raw.progressDetail);
  if (progressDetail) parsed.progressDetail = progressDetail;

  const error = toStringOrUndefined(raw.error);
  if (error) parsed.error = error;

  return parsed;
}

export async function pullDockerImage(
  image: string,
  onEvent?: (event: DockerPullEvent) => Promise<void> | void,
) {
  const path = `/images/create?fromImage=${encodeURIComponent(image)}`;

  await new Promise<void>((resolve, reject) => {
    const req = request(
      {
        socketPath: serverEnv.DOCKER_SOCKET_PATH,
        path,
        method: "POST",
        headers: {
          Host: "docker",
        },
      },
      (res) => {
        if (res.statusCode && res.statusCode >= 400) {
          let body = "";
          res.setEncoding("utf8");
          res.on("data", (chunk) => {
            body += chunk;
          });
          res.on("end", () => {
            reject(new Error(`Docker pull failed (${res.statusCode}): ${body}`));
          });
          return;
        }

        let buffered = "";

        const handleLine = async (line: string) => {
          const trimmed = line.trim();
          if (!trimmed) return;

          let raw: unknown;
          try {
            raw = JSON.parse(trimmed);
          } catch {
            return;
          }

          const event = parseDockerPullEvent(raw);
          if (!event) return;

          if (event.error) {
            throw new Error(event.error);
          }

          if (onEvent) {
            await onEvent(event);
          }
        };

        res.on("data", (chunk) => {
          buffered += chunk.toString("utf8");
          const lines = buffered.split("\n");
          buffered = lines.pop() ?? "";

          void (async () => {
            for (const line of lines) {
              await handleLine(line);
            }
          })().catch((error) => {
            req.destroy(error as Error);
          });
        });

        res.on("end", () => {
          void (async () => {
            if (buffered.trim().length > 0) {
              await handleLine(buffered);
            }
          })()
            .then(() => resolve())
            .catch(reject);
        });
      },
    );

    req.on("error", (error) => {
      reject(error);
    });

    req.end();
  });
}
byte
3.977
baris
165
Jenis
ts
Commit terakhir
16 Oktober 2026