diff --git a/Dockerfile b/Dockerfile index a2c148e..4b1bba0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,12 @@ COPY . . FROM workspace-source AS node-runtime WORKDIR /app +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libheif1 \ + libheif-plugin-libde265 \ + libde265-0 \ + && rm -rf /var/lib/apt/lists/* FROM workspace-source AS admin-build WORKDIR /app diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b73040c..48c4635 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,4 +1,8 @@ services: + api: + volumes: + - ./assets/import-library:/app/assets/import-library:ro + admin: build: context: . diff --git a/services/api/src/seed.ts b/services/api/src/seed.ts index 6fd9edf..6046fc7 100644 --- a/services/api/src/seed.ts +++ b/services/api/src/seed.ts @@ -10,7 +10,7 @@ interface ImportedAssetRecord { consent: ContributorConsent; } -const supportedExtensions = new Set([".jpg", ".jpeg", ".png", ".webp"]); +const supportedExtensions = new Set([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif"]); const repoRoot = path.dirname(config.storageDir); const importLibraryDir = path.join(repoRoot, "assets", "import-library"); export const libraryWatchDirs = [importLibraryDir]; @@ -135,63 +135,67 @@ export const createLibraryAssets = async () => { const imported: ImportedAssetRecord[] = []; for (const file of files) { - const baseName = path.parse(file.relativeName).name; - const baseId = `library-photo-${toSlug(baseName)}`; - const displayTitle = baseName.replace(/[-_]+/g, " "); - const originalRelativePath = path.join("runtime", "library", `${baseId}-original.jpg`); - const originalAbsolutePath = path.join(config.storageDir, originalRelativePath); + try { + const baseName = path.parse(file.relativeName).name; + const baseId = `library-photo-${toSlug(baseName)}`; + const displayTitle = baseName.replace(/[-_]+/g, " "); + const originalRelativePath = path.join("runtime", "library", `${baseId}-original.jpg`); + const originalAbsolutePath = path.join(config.storageDir, originalRelativePath); - const sourceImage = sharp(file.absolutePath).rotate(); - const metadata = await sourceImage.metadata(); - const width = metadata.width ?? 1600; - const height = metadata.height ?? 900; + const sourceImage = sharp(file.absolutePath).rotate(); + const metadata = await sourceImage.metadata(); + const width = metadata.width ?? 1600; + const height = metadata.height ?? 900; - await sourceImage.clone().jpeg({ quality: 92 }).toFile(originalAbsolutePath); - const dominantColor = await getDominantColor(sourceImage); - const thumbRelativePath = path.join("runtime", "library", `${baseId}-thumb.jpg`); - const previewRelativePath = path.join("runtime", "library", `${baseId}-preview.jpg`); - const renderRelativePath = path.join("runtime", "library", `${baseId}-render.jpg`); + await sourceImage.clone().jpeg({ quality: 92 }).toFile(originalAbsolutePath); + const dominantColor = await getDominantColor(sourceImage); + const thumbRelativePath = path.join("runtime", "library", `${baseId}-thumb.jpg`); + const previewRelativePath = path.join("runtime", "library", `${baseId}-preview.jpg`); + const renderRelativePath = path.join("runtime", "library", `${baseId}-render.jpg`); - await sourceImage - .clone() - .resize({ width: 320, height: 320, fit: "cover", position: "attention" }) - .jpeg({ quality: 84 }) - .toFile(path.join(config.storageDir, thumbRelativePath)); - await sourceImage - .clone() - .resize({ - width: 1280, - height: 1280, - fit: "inside", - withoutEnlargement: true - }) - .jpeg({ quality: 86 }) - .toFile(path.join(config.storageDir, previewRelativePath)); - await sourceImage - .clone() - .resize({ - width: 1920, - height: 1920, - fit: "inside", - withoutEnlargement: true - }) - .jpeg({ quality: 88 }) - .toFile(path.join(config.storageDir, renderRelativePath)); + await sourceImage + .clone() + .resize({ width: 320, height: 320, fit: "cover", position: "attention" }) + .jpeg({ quality: 84 }) + .toFile(path.join(config.storageDir, thumbRelativePath)); + await sourceImage + .clone() + .resize({ + width: 1280, + height: 1280, + fit: "inside", + withoutEnlargement: true + }) + .jpeg({ quality: 86 }) + .toFile(path.join(config.storageDir, previewRelativePath)); + await sourceImage + .clone() + .resize({ + width: 1920, + height: 1920, + fit: "inside", + withoutEnlargement: true + }) + .jpeg({ quality: 88 }) + .toFile(path.join(config.storageDir, renderRelativePath)); - imported.push( - toImportedRecord({ - id: baseId, - title: displayTitle, - originalKey: `/uploads/${originalRelativePath}`, - thumbKey: `/uploads/${thumbRelativePath}`, - previewKey: `/uploads/${previewRelativePath}`, - renderKey: `/uploads/${renderRelativePath}`, - mimeType: "image/jpeg", - width, - height, - dominantColor - }) - ); + imported.push( + toImportedRecord({ + id: baseId, + title: displayTitle, + originalKey: `/uploads/${originalRelativePath}`, + thumbKey: `/uploads/${thumbRelativePath}`, + previewKey: `/uploads/${previewRelativePath}`, + renderKey: `/uploads/${renderRelativePath}`, + mimeType: "image/jpeg", + width, + height, + dominantColor + }) + ); + } catch (error) { + console.warn(`[library-import] Skipping ${file.relativeName}:`, error); + } } return imported;