#!/usr/bin/env node // ─── Sync vault + carry to R2 ─────────────────────────────────────── // Walks the local vault and carry directories, uploads each file to R2 // under {did}/{relativePath}. // // Usage: // node scripts/sync-to-r2.mjs --did did:plc:xxx --vault /home/nandi/vault --carry /home/nandi/.local/share/carry-vault // // Requires R2 credentials as env vars: // R2_ACCOUNT_ID, R2_ACCESS_KEY, R2_SECRET_KEY import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { readdir, readFile, stat } from "fs/promises"; import { join, relative } from "path"; const args = process.argv.slice(2); function getArg(name: string): string | undefined { const idx = args.indexOf(`--${name}`); return idx >= 0 ? args[idx + 1] : undefined; } const DID = getArg("did") || process.env.STRATA_DID; const VAULT_DIR = getArg("vault") || process.env.VAULT_DIR || "/home/nandi/vault"; const CARRY_DIR = getArg("carry") || process.env.CARRY_DIR || "/home/nandi/.local/share/carry-vault"; const R2_ACCOUNT_ID = process.env.R2_ACCOUNT_ID; const R2_ACCESS_KEY = process.env.R2_ACCESS_KEY; const R2_SECRET_KEY = process.env.R2_SECRET_KEY; if (!DID) { console.error("Usage: node sync-to-r2.mjs --did did:plc:xxx [--vault path] [--carry path]"); console.error(" or set STRATA_DID, VAULT_DIR, CARRY_DIR env vars"); process.exit(1); } if (!R2_ACCOUNT_ID || !R2_ACCESS_KEY || !R2_SECRET_KEY) { console.error("Set R2_ACCOUNT_ID, R2_ACCESS_KEY, R2_SECRET_KEY env vars"); process.exit(1); } const s3 = new S3Client({ region: "auto", endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, credentials: { accessKeyId: R2_ACCESS_KEY, secretAccessKey: R2_SECRET_KEY, }, }); const VAULT_BUCKET = "stigmergic-vault"; const CARRY_BUCKET = "stigmergic-carry"; async function walkDir(dir: string): Promise { const files: string[] = []; try { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { files.push(...await walkDir(fullPath)); } else { files.push(fullPath); } } } catch { // Directory doesn't exist, skip } return files; } async function syncDir( baseDir: string, bucket: string, prefix: string, extensions: Set, ): Promise { const files = await walkDir(baseDir); let uploaded = 0; for (const filePath of files) { const relPath = relative(baseDir, filePath); const ext = relPath.split(".").pop() || ""; if (!extensions.has(ext)) continue; const key = `${prefix}/${relPath}`; const body = await readFile(filePath); try { await s3.send( new PutObjectCommand({ Bucket: bucket, Key: key, Body: body, ContentType: ext === "md" ? "text/markdown" : "application/json", }), ); uploaded++; console.log(` ✓ ${key}`); } catch (e: any) { console.error(` ✗ ${key}: ${e.message}`); } } return uploaded; } async function main() { console.log(`Syncing data for ${DID}`); console.log(); console.log("Syncing vault..."); const vaultCount = await syncDir(VAULT_DIR, VAULT_BUCKET, DID, new Set(["md", "canvas"])); console.log(` ${vaultCount} vault files uploaded`); console.log(); console.log("Syncing carry..."); const carryCount = await syncDir(CARRY_DIR, CARRY_BUCKET, DID, new Set(["json"])); console.log(` ${carryCount} carry files uploaded`); console.log(); console.log("Done."); } main().catch(console.error);