@open-mercato/core 0.6.2-develop.3381.1.8461b312a9 → 0.6.2-develop.3400.1.d948268f50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/attachments/api/file/[id]/route.js +8 -8
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -8
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +6 -2
- package/dist/modules/attachments/api/library/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/route.js +2 -0
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/openapi.js +5 -1
- package/dist/modules/attachments/api/openapi.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +10 -1
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +19 -7
- package/dist/modules/attachments/api/route.js.map +2 -2
- package/dist/modules/attachments/backend/config/attachments/page.js +3 -1
- package/dist/modules/attachments/backend/config/attachments/page.js.map +2 -2
- package/dist/modules/attachments/cli.js +6 -2
- package/dist/modules/attachments/cli.js.map +2 -2
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js +216 -10
- package/dist/modules/attachments/components/AttachmentPartitionSettings.js.map +2 -2
- package/dist/modules/attachments/di.js +13 -0
- package/dist/modules/attachments/di.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/driverFactory.js +67 -0
- package/dist/modules/attachments/lib/drivers/driverFactory.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/index.js +11 -0
- package/dist/modules/attachments/lib/drivers/index.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js +42 -0
- package/dist/modules/attachments/lib/drivers/legacyPublicDriver.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/localDriver.js +70 -0
- package/dist/modules/attachments/lib/drivers/localDriver.js.map +7 -0
- package/dist/modules/attachments/lib/drivers/types.js +1 -0
- package/dist/modules/attachments/lib/drivers/types.js.map +7 -0
- package/dist/modules/attachments/lib/ocrQueue.js +10 -5
- package/dist/modules/attachments/lib/ocrQueue.js.map +2 -2
- package/dist/modules/attachments/lib/storage.js.map +2 -2
- package/dist/modules/attachments/lib/thumbnailCache.js +11 -13
- package/dist/modules/attachments/lib/thumbnailCache.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/[id]/page.js +31 -5
- package/dist/modules/integrations/backend/integrations/[id]/page.js.map +2 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js +14 -2
- package/dist/modules/integrations/backend/integrations/bundle/[id]/page.js.map +2 -2
- package/package.json +11 -7
- package/src/modules/attachments/api/file/[id]/route.ts +10 -8
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +8 -8
- package/src/modules/attachments/api/library/[id]/route.ts +7 -3
- package/src/modules/attachments/api/library/route.ts +2 -0
- package/src/modules/attachments/api/openapi.ts +4 -0
- package/src/modules/attachments/api/partitions/route.ts +10 -1
- package/src/modules/attachments/api/route.ts +20 -7
- package/src/modules/attachments/backend/config/attachments/page.tsx +3 -1
- package/src/modules/attachments/cli.ts +7 -2
- package/src/modules/attachments/components/AttachmentPartitionSettings.tsx +275 -7
- package/src/modules/attachments/di.ts +13 -0
- package/src/modules/attachments/lib/drivers/driverFactory.ts +91 -0
- package/src/modules/attachments/lib/drivers/index.ts +4 -0
- package/src/modules/attachments/lib/drivers/legacyPublicDriver.ts +46 -0
- package/src/modules/attachments/lib/drivers/localDriver.ts +79 -0
- package/src/modules/attachments/lib/drivers/types.ts +28 -0
- package/src/modules/attachments/lib/ocrQueue.ts +14 -6
- package/src/modules/attachments/lib/storage.ts +13 -0
- package/src/modules/attachments/lib/thumbnailCache.ts +25 -19
- package/src/modules/integrations/backend/integrations/[id]/page.tsx +30 -4
- package/src/modules/integrations/backend/integrations/bundle/[id]/page.tsx +15 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
class LegacyPublicStorageDriver {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.key = "legacyPublic";
|
|
6
|
+
}
|
|
7
|
+
store(_payload) {
|
|
8
|
+
throw new Error("legacy-public driver is read-only");
|
|
9
|
+
}
|
|
10
|
+
async read(_partitionCode, storagePath) {
|
|
11
|
+
const absolutePath = this.resolveAbsolutePath(storagePath);
|
|
12
|
+
const buffer = await fs.readFile(absolutePath);
|
|
13
|
+
return { buffer };
|
|
14
|
+
}
|
|
15
|
+
async delete(_partitionCode, storagePath) {
|
|
16
|
+
const absolutePath = this.resolveAbsolutePath(storagePath);
|
|
17
|
+
try {
|
|
18
|
+
await fs.unlink(absolutePath);
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async toLocalPath(_partitionCode, storagePath) {
|
|
23
|
+
return {
|
|
24
|
+
filePath: this.resolveAbsolutePath(storagePath),
|
|
25
|
+
cleanup: async () => {
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
resolveAbsolutePath(storagePath) {
|
|
30
|
+
let safeRelative = storagePath.replace(/^\/*/, "");
|
|
31
|
+
let prev;
|
|
32
|
+
do {
|
|
33
|
+
prev = safeRelative;
|
|
34
|
+
safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
|
|
35
|
+
} while (safeRelative !== prev);
|
|
36
|
+
return path.join(process.cwd(), safeRelative);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
LegacyPublicStorageDriver
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=legacyPublicDriver.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/attachments/lib/drivers/legacyPublicDriver.ts"],
|
|
4
|
+
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nexport class LegacyPublicStorageDriver implements StorageDriver {\n readonly key = 'legacyPublic'\n\n store(_payload: StoreFilePayload): Promise<StoredFile> {\n throw new Error('legacy-public driver is read-only')\n }\n\n async read(_partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(_partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n _partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n return {\n filePath: this.resolveAbsolutePath(storagePath),\n cleanup: async () => {},\n }\n }\n\n private resolveAbsolutePath(storagePath: string): string {\n let safeRelative = storagePath.replace(/^\\/*/, '')\n let prev: string\n do {\n prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n return path.join(process.cwd(), safeRelative)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AAGV,MAAM,0BAAmD;AAAA,EAAzD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,UAAiD;AACrD,UAAM,IAAI,MAAM,mCAAmC;AAAA,EACrD;AAAA,EAEA,MAAM,KAAK,gBAAwB,aAA8C;AAC/E,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,gBAAwB,aAAoC;AACvE,UAAM,eAAe,KAAK,oBAAoB,WAAW;AACzD,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,gBACA,aAC6D;AAC7D,WAAO;AAAA,MACL,UAAU,KAAK,oBAAoB,WAAW;AAAA,MAC9C,SAAS,YAAY;AAAA,MAAC;AAAA,IACxB;AAAA,EACF;AAAA,EAEQ,oBAAoB,aAA6B;AACvD,QAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AACjD,QAAI;AACJ,OAAG;AACD,aAAO;AACP,qBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,IACxD,SAAS,iBAAiB;AAC1B,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { resolvePartitionRoot } from "../storage.js";
|
|
5
|
+
function sanitizeFileName(fileName) {
|
|
6
|
+
if (!fileName) return "file";
|
|
7
|
+
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
8
|
+
}
|
|
9
|
+
function resolveOrgSegment(orgId) {
|
|
10
|
+
if (typeof orgId === "string" && orgId.trim().length > 0) return `org_${orgId}`;
|
|
11
|
+
return "org_shared";
|
|
12
|
+
}
|
|
13
|
+
function resolveTenantSegment(tenantId) {
|
|
14
|
+
if (typeof tenantId === "string" && tenantId.trim().length > 0) return `tenant_${tenantId}`;
|
|
15
|
+
return "tenant_shared";
|
|
16
|
+
}
|
|
17
|
+
class LocalStorageDriver {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.key = "local";
|
|
20
|
+
}
|
|
21
|
+
async store(payload) {
|
|
22
|
+
const root = resolvePartitionRoot(payload.partitionCode);
|
|
23
|
+
const orgSegment = resolveOrgSegment(payload.orgId ?? null);
|
|
24
|
+
const tenantSegment = resolveTenantSegment(payload.tenantId ?? null);
|
|
25
|
+
const safeName = sanitizeFileName(payload.fileName || "file");
|
|
26
|
+
const uniqueSuffix = randomUUID().replace(/-/g, "").slice(0, 12);
|
|
27
|
+
const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`;
|
|
28
|
+
const relativePath = path.join(orgSegment, tenantSegment, storedName);
|
|
29
|
+
const absolutePath = path.join(root, relativePath);
|
|
30
|
+
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
31
|
+
await fs.writeFile(absolutePath, payload.buffer);
|
|
32
|
+
return {
|
|
33
|
+
storagePath: relativePath.replace(/\\/g, "/")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async read(partitionCode, storagePath) {
|
|
37
|
+
const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath);
|
|
38
|
+
const buffer = await fs.readFile(absolutePath);
|
|
39
|
+
return { buffer };
|
|
40
|
+
}
|
|
41
|
+
async delete(partitionCode, storagePath) {
|
|
42
|
+
const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath);
|
|
43
|
+
try {
|
|
44
|
+
await fs.unlink(absolutePath);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async toLocalPath(partitionCode, storagePath) {
|
|
49
|
+
const filePath = this.resolveAbsolutePath(partitionCode, storagePath);
|
|
50
|
+
return {
|
|
51
|
+
filePath,
|
|
52
|
+
cleanup: async () => {
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
resolveAbsolutePath(partitionCode, storagePath) {
|
|
57
|
+
let safeRelative = storagePath.replace(/^\/*/, "");
|
|
58
|
+
let prev;
|
|
59
|
+
do {
|
|
60
|
+
prev = safeRelative;
|
|
61
|
+
safeRelative = safeRelative.replace(/\.\.(\/|\\)/g, "");
|
|
62
|
+
} while (safeRelative !== prev);
|
|
63
|
+
const root = resolvePartitionRoot(partitionCode);
|
|
64
|
+
return path.join(root, safeRelative);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export {
|
|
68
|
+
LocalStorageDriver
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=localDriver.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/modules/attachments/lib/drivers/localDriver.ts"],
|
|
4
|
+
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionRoot } from '../storage'\nimport type { StorageDriver, StoreFilePayload, StoredFile, ReadFileResult } from './types'\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\nexport class LocalStorageDriver implements StorageDriver {\n readonly key = 'local'\n\n async store(payload: StoreFilePayload): Promise<StoredFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n }\n }\n\n async read(partitionCode: string, storagePath: string): Promise<ReadFileResult> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n const buffer = await fs.readFile(absolutePath)\n return { buffer }\n }\n\n async delete(partitionCode: string, storagePath: string): Promise<void> {\n const absolutePath = this.resolveAbsolutePath(partitionCode, storagePath)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n }\n\n async toLocalPath(\n partitionCode: string,\n storagePath: string,\n ): Promise<{ filePath: string; cleanup: () => Promise<void> }> {\n const filePath = this.resolveAbsolutePath(partitionCode, storagePath)\n return {\n filePath,\n cleanup: async () => {\n // no-op: local path is the real file, do not delete\n },\n }\n }\n\n private resolveAbsolutePath(partitionCode: string, storagePath: string): string {\n let safeRelative = storagePath.replace(/^\\/*/, '')\n let prev: string\n do {\n prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,4BAA4B;AAGrC,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAEO,MAAM,mBAA4C;AAAA,EAAlD;AACL,SAAS,MAAM;AAAA;AAAA,EAEf,MAAM,MAAM,SAAgD;AAC1D,UAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,UAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,UAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,UAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,UAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,UAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,UAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,UAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,UAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,UAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,WAAO;AAAA,MACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC9C;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,eAAuB,aAA8C;AAC9E,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,UAAM,SAAS,MAAM,GAAG,SAAS,YAAY;AAC7C,WAAO,EAAE,OAAO;AAAA,EAClB;AAAA,EAEA,MAAM,OAAO,eAAuB,aAAoC;AACtE,UAAM,eAAe,KAAK,oBAAoB,eAAe,WAAW;AACxE,QAAI;AACF,YAAM,GAAG,OAAO,YAAY;AAAA,IAC9B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,YACJ,eACA,aAC6D;AAC7D,UAAM,WAAW,KAAK,oBAAoB,eAAe,WAAW;AACpE,WAAO;AAAA,MACL;AAAA,MACA,SAAS,YAAY;AAAA,MAErB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,oBAAoB,eAAuB,aAA6B;AAC9E,QAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AACjD,QAAI;AACJ,OAAG;AACD,aAAO;AACP,qBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,IACxD,SAAS,iBAAiB;AAC1B,UAAM,OAAO,qBAAqB,aAAa;AAC/C,WAAO,KAAK,KAAK,MAAM,YAAY;AAAA,EACrC;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Attachment, AttachmentPartition } from "../data/entities.js";
|
|
2
2
|
import { OcrService } from "./ocrService.js";
|
|
3
|
-
async function processAttachmentOcr(em, payload) {
|
|
4
|
-
const { attachmentId,
|
|
3
|
+
async function processAttachmentOcr(em, payload, driver) {
|
|
4
|
+
const { attachmentId, storagePath, mimeType, partitionCode } = payload;
|
|
5
5
|
console.log(`[attachments.ocr] Processing started for attachment: ${attachmentId}`);
|
|
6
6
|
const startTime = Date.now();
|
|
7
|
+
const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath);
|
|
7
8
|
try {
|
|
8
9
|
const partition = await em.findOne(AttachmentPartition, { code: partitionCode });
|
|
9
10
|
const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? "gpt-4o";
|
|
@@ -40,12 +41,16 @@ async function processAttachmentOcr(em, payload) {
|
|
|
40
41
|
attachmentId,
|
|
41
42
|
error: error instanceof Error ? error.message : String(error)
|
|
42
43
|
});
|
|
44
|
+
} finally {
|
|
45
|
+
await cleanup().catch((cleanupError) => {
|
|
46
|
+
console.warn(`[attachments.ocr] Temp file cleanup failed:`, cleanupError);
|
|
47
|
+
});
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
|
-
async function requestOcrProcessing(em, attachment,
|
|
50
|
+
async function requestOcrProcessing(em, attachment, driver, storagePath) {
|
|
46
51
|
const payload = {
|
|
47
52
|
attachmentId: attachment.id,
|
|
48
|
-
|
|
53
|
+
storagePath,
|
|
49
54
|
mimeType: attachment.mimeType,
|
|
50
55
|
partitionCode: attachment.partitionCode,
|
|
51
56
|
organizationId: attachment.organizationId ?? null,
|
|
@@ -53,7 +58,7 @@ async function requestOcrProcessing(em, attachment, filePath) {
|
|
|
53
58
|
};
|
|
54
59
|
setImmediate(() => {
|
|
55
60
|
const workerEm = typeof em?.fork === "function" ? em.fork() : em;
|
|
56
|
-
processAttachmentOcr(workerEm, payload).catch((error) => {
|
|
61
|
+
processAttachmentOcr(workerEm, payload, driver).catch((error) => {
|
|
57
62
|
console.error(`[attachments.ocr] Background processing error:`, error);
|
|
58
63
|
});
|
|
59
64
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/ocrQueue.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { OcrService } from './ocrService'\n\nexport type OcrRequestedEvent = {\n attachmentId: string\n
|
|
5
|
-
"mappings": "AACA,SAAS,YAAY,2BAA2B;AAChD,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { Attachment, AttachmentPartition } from '../data/entities'\nimport { OcrService } from './ocrService'\nimport type { StorageDriver } from './drivers/types'\n\nexport type OcrRequestedEvent = {\n attachmentId: string\n storagePath: string\n mimeType: string\n partitionCode: string\n organizationId: string | null\n tenantId: string | null\n}\n\nexport async function processAttachmentOcr(\n em: EntityManager,\n payload: OcrRequestedEvent,\n driver: StorageDriver,\n): Promise<void> {\n const { attachmentId, storagePath, mimeType, partitionCode } = payload\n\n console.log(`[attachments.ocr] Processing started for attachment: ${attachmentId}`)\n const startTime = Date.now()\n\n const { filePath, cleanup } = await driver.toLocalPath(partitionCode, storagePath)\n try {\n const partition = await em.findOne(AttachmentPartition, { code: partitionCode })\n const resolvedModel = partition?.ocrModel ?? process.env.OCR_MODEL ?? 'gpt-4o'\n\n const ocrService = new OcrService()\n\n if (!ocrService.available) {\n console.warn(`[attachments.ocr] OPENAI_API_KEY not configured, skipping OCR for: ${attachmentId}`)\n return\n }\n\n const result = await ocrService.processFile({\n filePath,\n mimeType,\n model: resolvedModel,\n })\n\n if (!result) {\n console.log(`[attachments.ocr] No content extracted for attachment: ${attachmentId}`)\n return\n }\n\n const attachment = await em.findOne(Attachment, { id: attachmentId })\n if (!attachment) {\n console.error(`[attachments.ocr] Attachment not found: ${attachmentId}`)\n return\n }\n\n attachment.content = result.content\n await em.persist(attachment).flush()\n\n console.log(`[attachments.ocr] Processing completed:`, {\n attachmentId,\n pageCount: result.pageCount,\n contentLength: result.content.length,\n timeMs: result.processingTimeMs,\n totalTimeMs: Date.now() - startTime,\n })\n } catch (error) {\n console.error(`[attachments.ocr] Processing failed:`, {\n attachmentId,\n error: error instanceof Error ? error.message : String(error),\n })\n } finally {\n await cleanup().catch((cleanupError) => {\n console.warn(`[attachments.ocr] Temp file cleanup failed:`, cleanupError)\n })\n }\n}\n\nexport async function requestOcrProcessing(\n em: EntityManager,\n attachment: Attachment,\n driver: StorageDriver,\n storagePath: string,\n): Promise<void> {\n const payload: OcrRequestedEvent = {\n attachmentId: attachment.id,\n storagePath,\n mimeType: attachment.mimeType,\n partitionCode: attachment.partitionCode,\n organizationId: attachment.organizationId ?? null,\n tenantId: attachment.tenantId ?? null,\n }\n\n setImmediate(() => {\n const workerEm = typeof (em as any)?.fork === 'function' ? (em as any).fork() : em\n processAttachmentOcr(workerEm, payload, driver).catch((error) => {\n console.error(`[attachments.ocr] Background processing error:`, error)\n })\n })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,YAAY,2BAA2B;AAChD,SAAS,kBAAkB;AAY3B,eAAsB,qBACpB,IACA,SACA,QACe;AACf,QAAM,EAAE,cAAc,aAAa,UAAU,cAAc,IAAI;AAE/D,UAAQ,IAAI,wDAAwD,YAAY,EAAE;AAClF,QAAM,YAAY,KAAK,IAAI;AAE3B,QAAM,EAAE,UAAU,QAAQ,IAAI,MAAM,OAAO,YAAY,eAAe,WAAW;AACjF,MAAI;AACF,UAAM,YAAY,MAAM,GAAG,QAAQ,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC/E,UAAM,gBAAgB,WAAW,YAAY,QAAQ,IAAI,aAAa;AAEtE,UAAM,aAAa,IAAI,WAAW;AAElC,QAAI,CAAC,WAAW,WAAW;AACzB,cAAQ,KAAK,sEAAsE,YAAY,EAAE;AACjG;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,WAAW,YAAY;AAAA,MAC1C;AAAA,MACA;AAAA,MACA,OAAO;AAAA,IACT,CAAC;AAED,QAAI,CAAC,QAAQ;AACX,cAAQ,IAAI,0DAA0D,YAAY,EAAE;AACpF;AAAA,IACF;AAEA,UAAM,aAAa,MAAM,GAAG,QAAQ,YAAY,EAAE,IAAI,aAAa,CAAC;AACpE,QAAI,CAAC,YAAY;AACf,cAAQ,MAAM,2CAA2C,YAAY,EAAE;AACvE;AAAA,IACF;AAEA,eAAW,UAAU,OAAO;AAC5B,UAAM,GAAG,QAAQ,UAAU,EAAE,MAAM;AAEnC,YAAQ,IAAI,2CAA2C;AAAA,MACrD;AAAA,MACA,WAAW,OAAO;AAAA,MAClB,eAAe,OAAO,QAAQ;AAAA,MAC9B,QAAQ,OAAO;AAAA,MACf,aAAa,KAAK,IAAI,IAAI;AAAA,IAC5B,CAAC;AAAA,EACH,SAAS,OAAO;AACd,YAAQ,MAAM,wCAAwC;AAAA,MACpD;AAAA,MACA,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH,UAAE;AACA,UAAM,QAAQ,EAAE,MAAM,CAAC,iBAAiB;AACtC,cAAQ,KAAK,+CAA+C,YAAY;AAAA,IAC1E,CAAC;AAAA,EACH;AACF;AAEA,eAAsB,qBACpB,IACA,YACA,QACA,aACe;AACf,QAAM,UAA6B;AAAA,IACjC,cAAc,WAAW;AAAA,IACzB;AAAA,IACA,UAAU,WAAW;AAAA,IACrB,eAAe,WAAW;AAAA,IAC1B,gBAAgB,WAAW,kBAAkB;AAAA,IAC7C,UAAU,WAAW,YAAY;AAAA,EACnC;AAEA,eAAa,MAAM;AACjB,UAAM,WAAW,OAAQ,IAAY,SAAS,aAAc,GAAW,KAAK,IAAI;AAChF,yBAAqB,UAAU,SAAS,MAAM,EAAE,MAAM,CAAC,UAAU;AAC/D,cAAQ,MAAM,kDAAkD,KAAK;AAAA,IACvE,CAAC;AAAA,EACH,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/storage.ts"],
|
|
4
|
-
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(process.cwd(), 'storage', 'attachments', code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n // Remove leading slashes first\n let safeRelative = storagePath.replace(/^\\/*/, '')\n // Remove all ../ (and ..\\) path traversal segments, repeatedly until gone\n do {\n var prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n if (storageDriver === 'legacyPublic') {\n return path.join(process.cwd(), safeRelative)\n }\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n}\n\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AAEhC,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe,IAAI;AAChE;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;
|
|
4
|
+
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\nimport { randomUUID } from 'crypto'\nimport { resolvePartitionEnvKey } from './partitionEnv'\n\nexport function resolvePartitionRoot(code: string): string {\n const envKey = resolvePartitionEnvKey(code)\n const envPath = process.env[envKey]\n if (envPath && envPath.trim().length > 0) {\n return path.resolve(envPath)\n }\n return path.join(process.cwd(), 'storage', 'attachments', code)\n}\n\nfunction sanitizeFileName(fileName: string): string {\n if (!fileName) return 'file'\n return fileName.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\nfunction resolveOrgSegment(orgId: string | null | undefined): string {\n if (typeof orgId === 'string' && orgId.trim().length > 0) return `org_${orgId}`\n return 'org_shared'\n}\n\nfunction resolveTenantSegment(tenantId: string | null | undefined): string {\n if (typeof tenantId === 'string' && tenantId.trim().length > 0) return `tenant_${tenantId}`\n return 'tenant_shared'\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n * Kept for backward compatibility with external callers.\n */\nexport type StorePartitionFilePayload = {\n partitionCode: string\n orgId: string | null | undefined\n tenantId: string | null | undefined\n fileName: string\n buffer: Buffer\n}\n\nexport type StoredPartitionFile = {\n storagePath: string\n absolutePath: string\n fileName: string\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForPartition()` + `driver.store()` instead.\n */\nexport async function storePartitionFile(payload: StorePartitionFilePayload): Promise<StoredPartitionFile> {\n const root = resolvePartitionRoot(payload.partitionCode)\n const orgSegment = resolveOrgSegment(payload.orgId ?? null)\n const tenantSegment = resolveTenantSegment(payload.tenantId ?? null)\n const safeName = sanitizeFileName(payload.fileName || 'file')\n const uniqueSuffix = randomUUID().replace(/-/g, '').slice(0, 12)\n const storedName = `${Date.now()}_${uniqueSuffix}_${safeName}`\n const relativePath = path.join(orgSegment, tenantSegment, storedName)\n const absolutePath = path.join(root, relativePath)\n await fs.mkdir(path.dirname(absolutePath), { recursive: true })\n await fs.writeFile(absolutePath, payload.buffer)\n return {\n storagePath: relativePath.replace(/\\\\/g, '/'),\n absolutePath,\n fileName: storedName,\n }\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.read()` / `driver.toLocalPath()` instead.\n */\nexport function resolveAttachmentAbsolutePath(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): string {\n // Remove leading slashes first\n let safeRelative = storagePath.replace(/^\\/*/, '')\n // Remove all ../ (and ..\\) path traversal segments, repeatedly until gone\n do {\n var prev = safeRelative\n safeRelative = safeRelative.replace(/\\.\\.(\\/|\\\\)/g, '')\n } while (safeRelative !== prev)\n if (storageDriver === 'legacyPublic') {\n return path.join(process.cwd(), safeRelative)\n }\n const root = resolvePartitionRoot(partitionCode)\n return path.join(root, safeRelative)\n}\n\n/**\n * @deprecated Use `StorageDriverFactory.resolveForAttachment()` + `driver.delete()` instead.\n */\nexport async function deletePartitionFile(\n partitionCode: string,\n storagePath: string,\n storageDriver?: string | null\n): Promise<void> {\n const absolutePath = resolveAttachmentAbsolutePath(partitionCode, storagePath, storageDriver)\n try {\n await fs.unlink(absolutePath)\n } catch {\n // best-effort removal\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,kBAAkB;AAC3B,SAAS,8BAA8B;AAEhC,SAAS,qBAAqB,MAAsB;AACzD,QAAM,SAAS,uBAAuB,IAAI;AAC1C,QAAM,UAAU,QAAQ,IAAI,MAAM;AAClC,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACxC,WAAO,KAAK,QAAQ,OAAO;AAAA,EAC7B;AACA,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,eAAe,IAAI;AAChE;AAEA,SAAS,iBAAiB,UAA0B;AAClD,MAAI,CAAC,SAAU,QAAO;AACtB,SAAO,SAAS,QAAQ,oBAAoB,GAAG;AACjD;AAEA,SAAS,kBAAkB,OAA0C;AACnE,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,SAAS,EAAG,QAAO,OAAO,KAAK;AAC7E,SAAO;AACT;AAEA,SAAS,qBAAqB,UAA6C;AACzE,MAAI,OAAO,aAAa,YAAY,SAAS,KAAK,EAAE,SAAS,EAAG,QAAO,UAAU,QAAQ;AACzF,SAAO;AACT;AAuBA,eAAsB,mBAAmB,SAAkE;AACzG,QAAM,OAAO,qBAAqB,QAAQ,aAAa;AACvD,QAAM,aAAa,kBAAkB,QAAQ,SAAS,IAAI;AAC1D,QAAM,gBAAgB,qBAAqB,QAAQ,YAAY,IAAI;AACnE,QAAM,WAAW,iBAAiB,QAAQ,YAAY,MAAM;AAC5D,QAAM,eAAe,WAAW,EAAE,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,EAAE;AAC/D,QAAM,aAAa,GAAG,KAAK,IAAI,CAAC,IAAI,YAAY,IAAI,QAAQ;AAC5D,QAAM,eAAe,KAAK,KAAK,YAAY,eAAe,UAAU;AACpE,QAAM,eAAe,KAAK,KAAK,MAAM,YAAY;AACjD,QAAM,GAAG,MAAM,KAAK,QAAQ,YAAY,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9D,QAAM,GAAG,UAAU,cAAc,QAAQ,MAAM;AAC/C,SAAO;AAAA,IACL,aAAa,aAAa,QAAQ,OAAO,GAAG;AAAA,IAC5C;AAAA,IACA,UAAU;AAAA,EACZ;AACF;AAKO,SAAS,8BACd,eACA,aACA,eACQ;AAER,MAAI,eAAe,YAAY,QAAQ,QAAQ,EAAE;AAEjD,KAAG;AACD,QAAI,OAAO;AACX,mBAAe,aAAa,QAAQ,gBAAgB,EAAE;AAAA,EACxD,SAAS,iBAAiB;AAC1B,MAAI,kBAAkB,gBAAgB;AACpC,WAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,YAAY;AAAA,EAC9C;AACA,QAAM,OAAO,qBAAqB,aAAa;AAC/C,SAAO,KAAK,KAAK,MAAM,YAAY;AACrC;AAKA,eAAsB,oBACpB,eACA,aACA,eACe;AACf,QAAM,eAAe,8BAA8B,eAAe,aAAa,aAAa;AAC5F,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;AAAA,EAC9B,QAAQ;AAAA,EAER;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { resolvePartitionRoot } from "./storage.js";
|
|
4
|
-
const CACHE_ROOT_SEGMENTS = [".cache", "thumbnails"];
|
|
5
3
|
function sanitizeSegment(value) {
|
|
6
4
|
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
7
5
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
const safeWidth = typeof width === "number" && Number.isFinite(width) ? width : "auto";
|
|
11
|
-
const safeHeight = typeof height === "number" && Number.isFinite(height) ? height : "auto";
|
|
12
|
-
const safeCrop = cropType === "contain" ? "contain" : "cover";
|
|
13
|
-
return `w${safeWidth}-h${safeHeight}-c${safeCrop}`;
|
|
6
|
+
function resolveCacheRoot(partitionCode) {
|
|
7
|
+
return path.join(process.cwd(), "storage", ".cache", "thumbnails", sanitizeSegment(partitionCode));
|
|
14
8
|
}
|
|
15
9
|
function resolveCachePath(partitionCode, attachmentId, cacheKey) {
|
|
16
|
-
const root = resolvePartitionRoot(partitionCode);
|
|
17
10
|
return path.join(
|
|
18
|
-
|
|
19
|
-
...CACHE_ROOT_SEGMENTS,
|
|
11
|
+
resolveCacheRoot(partitionCode),
|
|
20
12
|
sanitizeSegment(attachmentId),
|
|
21
13
|
sanitizeSegment(cacheKey)
|
|
22
14
|
);
|
|
23
15
|
}
|
|
16
|
+
function buildThumbnailCacheKey(width, height, cropType) {
|
|
17
|
+
if (!width && !height) return null;
|
|
18
|
+
const safeWidth = typeof width === "number" && Number.isFinite(width) ? width : "auto";
|
|
19
|
+
const safeHeight = typeof height === "number" && Number.isFinite(height) ? height : "auto";
|
|
20
|
+
const safeCrop = cropType === "contain" ? "contain" : "cover";
|
|
21
|
+
return `w${safeWidth}-h${safeHeight}-c${safeCrop}`;
|
|
22
|
+
}
|
|
24
23
|
async function readThumbnailCache(partitionCode, attachmentId, cacheKey) {
|
|
25
24
|
const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey);
|
|
26
25
|
try {
|
|
@@ -35,8 +34,7 @@ async function writeThumbnailCache(partitionCode, attachmentId, cacheKey, data)
|
|
|
35
34
|
await fs.writeFile(cachePath, data);
|
|
36
35
|
}
|
|
37
36
|
async function clearAttachmentThumbnailCache(partitionCode, attachmentId) {
|
|
38
|
-
const
|
|
39
|
-
const dir = path.join(root, ...CACHE_ROOT_SEGMENTS, sanitizeSegment(attachmentId));
|
|
37
|
+
const dir = path.join(resolveCacheRoot(partitionCode), sanitizeSegment(attachmentId));
|
|
40
38
|
try {
|
|
41
39
|
await fs.rm(dir, { recursive: true, force: true });
|
|
42
40
|
} catch {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/attachments/lib/thumbnailCache.ts"],
|
|
4
|
-
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;
|
|
4
|
+
"sourcesContent": ["import { promises as fs } from 'fs'\nimport path from 'path'\n\nfunction sanitizeSegment(value: string): string {\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\n/**\n * Resolves the thumbnail cache root. Uses a dedicated directory under\n * `storage/.cache/thumbnails/<partitionCode>/` that is independent of\n * the partition's storage backend (local or S3).\n */\nfunction resolveCacheRoot(partitionCode: string): string {\n return path.join(process.cwd(), 'storage', '.cache', 'thumbnails', sanitizeSegment(partitionCode))\n}\n\nfunction resolveCachePath(partitionCode: string, attachmentId: string, cacheKey: string): string {\n return path.join(\n resolveCacheRoot(partitionCode),\n sanitizeSegment(attachmentId),\n sanitizeSegment(cacheKey),\n )\n}\n\nexport function buildThumbnailCacheKey(\n width?: number,\n height?: number,\n cropType?: 'cover' | 'contain',\n): string | null {\n if (!width && !height) return null\n const safeWidth = typeof width === 'number' && Number.isFinite(width) ? width : 'auto'\n const safeHeight = typeof height === 'number' && Number.isFinite(height) ? height : 'auto'\n const safeCrop = cropType === 'contain' ? 'contain' : 'cover'\n return `w${safeWidth}-h${safeHeight}-c${safeCrop}`\n}\n\nexport async function readThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n cacheKey: string,\n): Promise<Buffer | null> {\n const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)\n try {\n return await fs.readFile(cachePath)\n } catch {\n return null\n }\n}\n\nexport async function writeThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n cacheKey: string,\n data: Buffer,\n): Promise<void> {\n const cachePath = resolveCachePath(partitionCode, attachmentId, cacheKey)\n await fs.mkdir(path.dirname(cachePath), { recursive: true })\n await fs.writeFile(cachePath, data)\n}\n\nexport async function clearAttachmentThumbnailCache(\n partitionCode: string,\n attachmentId: string,\n): Promise<void> {\n const dir = path.join(resolveCacheRoot(partitionCode), sanitizeSegment(attachmentId))\n try {\n await fs.rm(dir, { recursive: true, force: true })\n } catch {\n // ignore cache cleanup failure\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AAEjB,SAAS,gBAAgB,OAAuB;AAC9C,SAAO,MAAM,QAAQ,oBAAoB,GAAG;AAC9C;AAOA,SAAS,iBAAiB,eAA+B;AACvD,SAAO,KAAK,KAAK,QAAQ,IAAI,GAAG,WAAW,UAAU,cAAc,gBAAgB,aAAa,CAAC;AACnG;AAEA,SAAS,iBAAiB,eAAuB,cAAsB,UAA0B;AAC/F,SAAO,KAAK;AAAA,IACV,iBAAiB,aAAa;AAAA,IAC9B,gBAAgB,YAAY;AAAA,IAC5B,gBAAgB,QAAQ;AAAA,EAC1B;AACF;AAEO,SAAS,uBACd,OACA,QACA,UACe;AACf,MAAI,CAAC,SAAS,CAAC,OAAQ,QAAO;AAC9B,QAAM,YAAY,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,IAAI,QAAQ;AAChF,QAAM,aAAa,OAAO,WAAW,YAAY,OAAO,SAAS,MAAM,IAAI,SAAS;AACpF,QAAM,WAAW,aAAa,YAAY,YAAY;AACtD,SAAO,IAAI,SAAS,KAAK,UAAU,KAAK,QAAQ;AAClD;AAEA,eAAsB,mBACpB,eACA,cACA,UACwB;AACxB,QAAM,YAAY,iBAAiB,eAAe,cAAc,QAAQ;AACxE,MAAI;AACF,WAAO,MAAM,GAAG,SAAS,SAAS;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAsB,oBACpB,eACA,cACA,UACA,MACe;AACf,QAAM,YAAY,iBAAiB,eAAe,cAAc,QAAQ;AACxE,QAAM,GAAG,MAAM,KAAK,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;AAC3D,QAAM,GAAG,UAAU,WAAW,IAAI;AACpC;AAEA,eAAsB,8BACpB,eACA,cACe;AACf,QAAM,MAAM,KAAK,KAAK,iBAAiB,aAAa,GAAG,gBAAgB,YAAY,CAAC;AACpF,MAAI;AACF,UAAM,GAAG,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACnD,QAAQ;AAAA,EAER;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -176,7 +176,8 @@ function buildCredentialFields(credFields) {
|
|
|
176
176
|
/* @__PURE__ */ jsx(WebhookSetupGuide, { guide: field.helpDetails, buttonLabel: "Show details" })
|
|
177
177
|
] }) : field.helpText,
|
|
178
178
|
placeholder: field.placeholder,
|
|
179
|
-
required: field.required
|
|
179
|
+
required: field.required,
|
|
180
|
+
visibleWhen: field.visibleWhen
|
|
180
181
|
};
|
|
181
182
|
if (field.type === "secret") {
|
|
182
183
|
return {
|
|
@@ -326,7 +327,15 @@ function IntegrationDetailPage({ params }) {
|
|
|
326
327
|
{ fallback: null }
|
|
327
328
|
);
|
|
328
329
|
if (call.ok && call.result?.credentials) {
|
|
329
|
-
|
|
330
|
+
const next = { ...call.result.credentials };
|
|
331
|
+
if (currentIntegrationId === "storage_s3") {
|
|
332
|
+
const authMode = next.authMode;
|
|
333
|
+
if (authMode !== "access_keys" && authMode !== "ambient") {
|
|
334
|
+
const hasKeys = Boolean(next.accessKeyId || next.secretAccessKey);
|
|
335
|
+
next.authMode = hasKeys ? "access_keys" : "ambient";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
setCredValues(next);
|
|
330
339
|
setCredentialsFormKey((current) => current + 1);
|
|
331
340
|
}
|
|
332
341
|
}, [resolveCurrentIntegrationId]);
|
|
@@ -517,18 +526,31 @@ function IntegrationDetailPage({ params }) {
|
|
|
517
526
|
if (!currentIntegrationId) return;
|
|
518
527
|
setIsSavingCredentials(true);
|
|
519
528
|
try {
|
|
529
|
+
const sanitizedValues = { ...values };
|
|
530
|
+
if (currentIntegrationId === "storage_s3") {
|
|
531
|
+
const authMode = sanitizedValues.authMode;
|
|
532
|
+
if (authMode !== "access_keys" && authMode !== "ambient") {
|
|
533
|
+
const hasKeys = Boolean(sanitizedValues.accessKeyId || sanitizedValues.secretAccessKey);
|
|
534
|
+
sanitizedValues.authMode = hasKeys ? "access_keys" : "ambient";
|
|
535
|
+
}
|
|
536
|
+
if (sanitizedValues.authMode === "ambient") {
|
|
537
|
+
delete sanitizedValues.accessKeyId;
|
|
538
|
+
delete sanitizedValues.secretAccessKey;
|
|
539
|
+
delete sanitizedValues.sessionToken;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
520
542
|
const call = await runMutationWithContext({
|
|
521
543
|
actionId: "save-credentials",
|
|
522
544
|
tabId: "credentials",
|
|
523
|
-
mutationPayload: { integrationId: currentIntegrationId, credentials:
|
|
545
|
+
mutationPayload: { integrationId: currentIntegrationId, credentials: sanitizedValues },
|
|
524
546
|
operation: () => apiCall(`/api/integrations/${encodeURIComponent(currentIntegrationId)}/credentials`, {
|
|
525
547
|
method: "PUT",
|
|
526
548
|
headers: { "Content-Type": "application/json" },
|
|
527
|
-
body: JSON.stringify({ credentials:
|
|
549
|
+
body: JSON.stringify({ credentials: sanitizedValues })
|
|
528
550
|
}, { fallback: null })
|
|
529
551
|
});
|
|
530
552
|
if (call.ok) {
|
|
531
|
-
setCredValues(
|
|
553
|
+
setCredValues(sanitizedValues);
|
|
532
554
|
setCredentialsFormKey((current) => current + 1);
|
|
533
555
|
flash(t("integrations.detail.credentials.saved"), "success");
|
|
534
556
|
return;
|
|
@@ -619,6 +641,10 @@ function IntegrationDetailPage({ params }) {
|
|
|
619
641
|
const credentialSchema = React.useMemo(() => z.object({}).passthrough().superRefine((rawValues, ctx) => {
|
|
620
642
|
const values = rawValues;
|
|
621
643
|
editableCredentialFields.forEach((field) => {
|
|
644
|
+
if (field.visibleWhen) {
|
|
645
|
+
const targetValue = values[field.visibleWhen.field];
|
|
646
|
+
if (targetValue !== field.visibleWhen.equals) return;
|
|
647
|
+
}
|
|
622
648
|
const value = values[field.key];
|
|
623
649
|
if (field.type === "boolean") {
|
|
624
650
|
if (value !== void 0 && value !== null && typeof value !== "boolean") {
|