Commit 86db3c0c authored by Jiwon Kwon's avatar Jiwon Kwon
Browse files

feat: add type for command option, export write&create file func

parent 90c3bd6f
Loading
Loading
Loading
Loading
+107 −79
Original line number Diff line number Diff line
@@ -25,6 +25,106 @@ const logger = getLogger(["fedify", "cli", "lookup"]);

const sigSpec = new EnumType(["draft-cavage-http-signatures-12", "rfc9421"]);

interface CommandOptions {
  authorizedFetch?: boolean;
  firstKnock: "draft-cavage-http-signatures-12" | "rfc9421";
  traverse?: boolean;
  suppressErrors?: boolean;
  raw?: boolean;
  compact?: boolean;
  expand?: boolean;
  userAgent?: string;
  separator: string;
  output?: string;
}

export async function createFileStream(
  outputPath: string,
): Promise<WritableStream> {
  try {
    const filepath = isAbsolute(outputPath)
      ? outputPath
      : resolve(Deno.env.get("PWD") || Deno.cwd(), outputPath);

    const parentDir = dirname(filepath);
    await Deno.mkdir(parentDir, { recursive: true });

    const file = await Deno.open(filepath, {
      write: true,
      create: true,
      truncate: true,
    });

    return new WritableStream({
      write: (chunk) => file.write(chunk).then(() => {}),
      close: () => file.close(),
      abort: (reason) => {
        file.close();
        throw reason;
      },
    });
  } catch (err) {
    const spinner = ora({
      text: `Failed to write output to ${colors.red(outputPath)}.`,
      discardStdin: false,
    });
    spinner.fail();
    console.error(`Error: ${String(err)}`);

    if (err instanceof Deno.errors.PermissionDenied) {
      console.error(
        "Permission denied. Try running with proper permissions.",
      );
    } else if (err instanceof Deno.errors.NotFound) {
      console.error("Path does not exist or is invalid.");
    } else if (err instanceof Deno.errors.IsADirectory) {
      console.error("The specified path is a directory, not a file.");
    }
    Deno.exit(1);
  }
}

export async function writeObjectToStream(
  object: Object | Link,
  options: CommandOptions,
  contextLoader: DocumentLoader,
): Promise<void> {
  const stream = options.output
    ? await createFileStream(options.output)
    : Deno.stdout.writable;

  const writer = stream.getWriter();

  try {
    let content;

    if (options.raw) {
      content = await object.toJsonLd({ contextLoader });
    } else if (options.compact) {
      content = await object.toJsonLd({ format: "compact", contextLoader });
    } else if (options.expand) {
      content = await object.toJsonLd({ format: "expand", contextLoader });
    } else {
      content = object;
    }

    content = util.inspect(content, {
      depth: null,
      colors: !(options.output),
    });

    const encoder = new TextEncoder();
    const bytes = encoder.encode(content + "\n");

    await writer.write(bytes);
  } finally {
    writer.releaseLock();
    if (options.output) {
      await stream.close();
    }
  }
}

export const command = new Command()
  .type("sig-spec", sigSpec)
  .arguments("<...urls:string>")
@@ -154,83 +254,6 @@ export const command = new Command()
      options.traverse ? "collection" : urls.length > 1 ? "objects" : "object"
    }...`;

    async function writeObjectToStream(
      object: Object | Link,
    ): Promise<void> {
      const stream = options.output
        ? await createFileStream(options.output)
        : Deno.stdout.writable;

      const writer = stream.getWriter();

      try {
        let content;

        if (options.raw) {
          content = await object.toJsonLd({ contextLoader });
        } else if (options.compact) {
          content = await object.toJsonLd({ format: "compact", contextLoader });
        } else if (options.expand) {
          content = await object.toJsonLd({ format: "expand", contextLoader });
        } else {
          content = object;
        }

        content = util.inspect(content, {
          depth: null,
          colors: !(stream instanceof WritableStream),
        });

        const encoder = new TextEncoder();
        const bytes = encoder.encode(content + "\n");

        await writer.write(bytes);
      } finally {
        writer.releaseLock();
      }
    }
    async function createFileStream(
      outputPath: string,
    ): Promise<WritableStream> {
      try {
        const filepath = isAbsolute(outputPath)
          ? outputPath
          : resolve(Deno.env.get("PWD") || Deno.cwd(), outputPath);
        spinner.start(`Writing output to ${colors.green(filepath)}...`);
        const parentDir = dirname(filepath);
        await Deno.mkdir(parentDir, { recursive: true });

        const file = await Deno.open(filepath, {
          write: true,
          create: true,
          truncate: true,
        });

        return new WritableStream({
          write: (chunk) => file.write(chunk).then(() => {}),
          close: () => file.close(),
          abort: (reason) => {
            file.close();
            throw reason;
          },
        });
      } catch (err) {
        spinner.fail(`Failed to write output.`);
        console.error(`Error: ${String(err)}`);

        if (err instanceof Deno.errors.PermissionDenied) {
          console.error(
            "Permission denied. Try running with proper permissions.",
          );
        } else if (err instanceof Deno.errors.NotFound) {
          console.error("Path does not exist or is invalid.");
        } else if (err instanceof Deno.errors.IsADirectory) {
          console.error("The specified path is a directory, not a file.");
        }
        Deno.exit(1);
      }
    }

    if (options.traverse) {
      const url = urls[0];
      const collection = await lookupObject(url, {
@@ -267,7 +290,7 @@ export const command = new Command()
          })
        ) {
          if (!options.output && i > 0) console.log(options.separator);
          await writeObjectToStream(item);
          await writeObjectToStream(item, options, contextLoader);
          i++;
        }
      } catch (error) {
@@ -323,7 +346,7 @@ export const command = new Command()
          success = false;
        } else {
          spinner.succeed(`Fetched object: ${colors.green(url)}.`);
          await writeObjectToStream(object);
          await writeObjectToStream(object, options, contextLoader);
          if (i < urls.length - 1) {
            console.log(options.separator);
          }
@@ -343,6 +366,11 @@ export const command = new Command()
    if (!success) {
      Deno.exit(1);
    }
    if (success && options.output) {
      spinner.succeed(
        `Successfully wrote output to ${colors.green(options.output)}.`,
      );
    }
  });

// cSpell: ignore sigspec