Search sibling storage roots for attachments
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
fb63d61ae5
commit
54b99d5096
2 changed files with 68 additions and 15 deletions
|
|
@ -196,13 +196,37 @@ describe("worktree helpers", () => {
|
||||||
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
|
expect(formatShellExports(env)).toContain("export PAPERCLIP_INSTANCE_ID='feature-worktree-support'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("treats missing source attachment objects as a non-fatal skip", async () => {
|
it("falls back across storage roots before skipping a missing attachment object", async () => {
|
||||||
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
|
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
|
||||||
|
const expected = Buffer.from("image-bytes");
|
||||||
await expect(
|
await expect(
|
||||||
readSourceAttachmentBody(
|
readSourceAttachmentBody(
|
||||||
|
[
|
||||||
{
|
{
|
||||||
getObject: vi.fn().mockRejectedValue(missingErr),
|
getObject: vi.fn().mockRejectedValue(missingErr),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
getObject: vi.fn().mockResolvedValue(expected),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"company-1",
|
||||||
|
"company-1/issues/issue-1/missing.png",
|
||||||
|
),
|
||||||
|
).resolves.toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when an attachment object is missing from every lookup storage", async () => {
|
||||||
|
const missingErr = Object.assign(new Error("missing"), { code: "ENOENT" });
|
||||||
|
await expect(
|
||||||
|
readSourceAttachmentBody(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
getObject: vi.fn().mockRejectedValue(missingErr),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getObject: vi.fn().mockRejectedValue(Object.assign(new Error("missing"), { status: 404 })),
|
||||||
|
},
|
||||||
|
],
|
||||||
"company-1",
|
"company-1",
|
||||||
"company-1/issues/issue-1/missing.png",
|
"company-1/issues/issue-1/missing.png",
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -360,19 +360,22 @@ export function isMissingStorageObjectError(error: unknown): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readSourceAttachmentBody(
|
export async function readSourceAttachmentBody(
|
||||||
sourceStorage: Pick<ConfiguredStorage, "getObject">,
|
sourceStorages: Array<Pick<ConfiguredStorage, "getObject">>,
|
||||||
companyId: string,
|
companyId: string,
|
||||||
objectKey: string,
|
objectKey: string,
|
||||||
): Promise<Buffer | null> {
|
): Promise<Buffer | null> {
|
||||||
|
for (const sourceStorage of sourceStorages) {
|
||||||
try {
|
try {
|
||||||
return await sourceStorage.getObject(companyId, objectKey);
|
return await sourceStorage.getObject(companyId, objectKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isMissingStorageObjectError(error)) {
|
if (isMissingStorageObjectError(error)) {
|
||||||
return null;
|
continue;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveWorktreeMakeTargetPath(name: string): string {
|
export function resolveWorktreeMakeTargetPath(name: string): string {
|
||||||
return path.resolve(os.homedir(), resolveWorktreeMakeName(name));
|
return path.resolve(os.homedir(), resolveWorktreeMakeName(name));
|
||||||
|
|
@ -1350,6 +1353,29 @@ function resolveCurrentEndpoint(): ResolvedWorktreeEndpoint {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAttachmentLookupStorages(input: {
|
||||||
|
sourceEndpoint: ResolvedWorktreeEndpoint;
|
||||||
|
targetEndpoint: ResolvedWorktreeEndpoint;
|
||||||
|
}): ConfiguredStorage[] {
|
||||||
|
const orderedConfigPaths = [
|
||||||
|
input.sourceEndpoint.configPath,
|
||||||
|
resolveCurrentEndpoint().configPath,
|
||||||
|
input.targetEndpoint.configPath,
|
||||||
|
...toMergeSourceChoices(process.cwd())
|
||||||
|
.filter((choice) => choice.hasPaperclipConfig)
|
||||||
|
.map((choice) => path.resolve(choice.worktree, ".paperclip", "config.json")),
|
||||||
|
];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const storages: ConfiguredStorage[] = [];
|
||||||
|
for (const configPath of orderedConfigPaths) {
|
||||||
|
const resolved = path.resolve(configPath);
|
||||||
|
if (seen.has(resolved) || !existsSync(resolved)) continue;
|
||||||
|
seen.add(resolved);
|
||||||
|
storages.push(openConfiguredStorage(resolved));
|
||||||
|
}
|
||||||
|
return storages;
|
||||||
|
}
|
||||||
|
|
||||||
async function openConfiguredDb(configPath: string): Promise<OpenDbHandle> {
|
async function openConfiguredDb(configPath: string): Promise<OpenDbHandle> {
|
||||||
const config = readConfig(configPath);
|
const config = readConfig(configPath);
|
||||||
if (!config) {
|
if (!config) {
|
||||||
|
|
@ -1930,7 +1956,7 @@ async function promptForSourceEndpoint(excludeWorktreePath?: string): Promise<Re
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyMergePlan(input: {
|
async function applyMergePlan(input: {
|
||||||
sourceStorage: ConfiguredStorage;
|
sourceStorages: ConfiguredStorage[];
|
||||||
targetStorage: ConfiguredStorage;
|
targetStorage: ConfiguredStorage;
|
||||||
targetDb: ClosableDb;
|
targetDb: ClosableDb;
|
||||||
company: ResolvedMergeCompany;
|
company: ResolvedMergeCompany;
|
||||||
|
|
@ -2194,7 +2220,7 @@ async function applyMergePlan(input: {
|
||||||
if (!parentExists) continue;
|
if (!parentExists) continue;
|
||||||
|
|
||||||
const body = await readSourceAttachmentBody(
|
const body = await readSourceAttachmentBody(
|
||||||
input.sourceStorage,
|
input.sourceStorages,
|
||||||
companyId,
|
companyId,
|
||||||
attachment.source.objectKey,
|
attachment.source.objectKey,
|
||||||
);
|
);
|
||||||
|
|
@ -2274,7 +2300,10 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
|
||||||
const scopes = parseWorktreeMergeScopes(opts.scope);
|
const scopes = parseWorktreeMergeScopes(opts.scope);
|
||||||
const sourceHandle = await openConfiguredDb(sourceEndpoint.configPath);
|
const sourceHandle = await openConfiguredDb(sourceEndpoint.configPath);
|
||||||
const targetHandle = await openConfiguredDb(targetEndpoint.configPath);
|
const targetHandle = await openConfiguredDb(targetEndpoint.configPath);
|
||||||
const sourceStorage = openConfiguredStorage(sourceEndpoint.configPath);
|
const sourceStorages = resolveAttachmentLookupStorages({
|
||||||
|
sourceEndpoint,
|
||||||
|
targetEndpoint,
|
||||||
|
});
|
||||||
const targetStorage = openConfiguredStorage(targetEndpoint.configPath);
|
const targetStorage = openConfiguredStorage(targetEndpoint.configPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -2328,7 +2357,7 @@ export async function worktreeMergeHistoryCommand(sourceArg: string | undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const applied = await applyMergePlan({
|
const applied = await applyMergePlan({
|
||||||
sourceStorage,
|
sourceStorages,
|
||||||
targetStorage,
|
targetStorage,
|
||||||
targetDb: targetHandle.db,
|
targetDb: targetHandle.db,
|
||||||
company,
|
company,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue