文件预览

storage.ts

查看 ClawSync 技能包中的文件内容。

文件内容

cloud-api/src/storage.ts

import fs from "node:fs/promises";
import path from "node:path";
import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
  DeleteObjectCommand,
} from "@aws-sdk/client-s3";

const useS3 = !!process.env.S3_ENDPOINT;

// ─── S3 backend ──────────────────────────────────────────────

const s3 = useS3
  ? new S3Client({
      endpoint: process.env.S3_ENDPOINT,
      region: process.env.S3_REGION || "auto",
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY || "",
        secretAccessKey: process.env.S3_SECRET_KEY || "",
      },
      forcePathStyle: true,
    })
  : null;

const bucket = process.env.S3_BUCKET || "clawsync";

// ─── Filesystem backend (local dev / no S3 configured) ──────

const FS_ROOT = process.env.STORAGE_PATH || "/tmp/clawsync-storage";

async function ensureDir(filePath: string) {
  await fs.mkdir(path.dirname(filePath), { recursive: true });
}

// ─── Public API ──────────────────────────────────────────────

function vaultKey(vaultId: string, version: string): string {
  return `vaults/${vaultId}/${version}.tar.gz`;
}

export async function uploadArchive(
  vaultId: string,
  versionId: string,
  data: Buffer | Uint8Array
): Promise<string> {
  const key = vaultKey(vaultId, versionId);

  if (s3) {
    await s3.send(
      new PutObjectCommand({
        Bucket: bucket,
        Key: key,
        Body: data,
        ContentType: "application/gzip",
      })
    );
  } else {
    const filePath = path.join(FS_ROOT, key);
    await ensureDir(filePath);
    await fs.writeFile(filePath, data);
  }

  return key;
}

export async function downloadArchive(
  s3Key: string
): Promise<Uint8Array | null> {
  if (s3) {
    try {
      const res = await s3.send(
        new GetObjectCommand({ Bucket: bucket, Key: s3Key })
      );
      if (!res.Body) return null;
      return await res.Body.transformToByteArray();
    } catch (err: unknown) {
      if (err instanceof Error && err.name === "NoSuchKey") return null;
      throw err;
    }
  } else {
    try {
      return await fs.readFile(path.join(FS_ROOT, s3Key));
    } catch {
      return null;
    }
  }
}

export async function deleteArchive(s3Key: string): Promise<void> {
  if (s3) {
    await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: s3Key }));
  } else {
    await fs.unlink(path.join(FS_ROOT, s3Key)).catch(() => {});
  }
}