feat: compression 11
This commit is contained in:
@@ -2,10 +2,11 @@ import { ENV } from "@/config/environment";
|
||||
import { db } from "@/config/db";
|
||||
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
|
||||
import { getUserHeaders } from "@/helpers/user.helper";
|
||||
import { clearCompressionDirectory, getCompressionOutputPath, getVideoContentType } from "@/helpers/compression.server";
|
||||
import { clearCompressionDirectory, getCompressionOutputPath } from "@/helpers/compression.server";
|
||||
import { assets } from "@/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { openAsBlob } from "node:fs";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
@@ -14,6 +15,24 @@ const readError = async (response: Response) => {
|
||||
return body || response.statusText || `HTTP ${response.status}`;
|
||||
};
|
||||
|
||||
class UploadFile extends File {
|
||||
constructor(
|
||||
private readonly filePath: string,
|
||||
private readonly fileSize: number,
|
||||
fileName: string,
|
||||
) {
|
||||
super([], fileName);
|
||||
}
|
||||
|
||||
override get size() {
|
||||
return this.fileSize;
|
||||
}
|
||||
|
||||
override stream() {
|
||||
return createReadStream(this.filePath) as any;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" });
|
||||
|
||||
@@ -65,18 +84,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
|
||||
const replacementFileName = path.basename(source.originalFileName || dbAsset.originalFileName || `${dbAsset.id}.mp4`);
|
||||
const outputPath = getCompressionOutputPath(dbAsset.id, dbAsset.originalFileName);
|
||||
const fileBlob = await openAsBlob(outputPath, { type: getVideoContentType(outputPath) });
|
||||
const outputStats = await stat(outputPath);
|
||||
const uploadFile = new UploadFile(outputPath, outputStats.size, replacementFileName);
|
||||
|
||||
// Keep the upload payload aligned with Immich's own CLI uploader.
|
||||
// Restore Power Tools/custom metadata only after Immich has created the asset.
|
||||
const formData = new FormData();
|
||||
formData.set("fileCreatedAt", source.fileCreatedAt);
|
||||
formData.set("fileModifiedAt", source.fileModifiedAt);
|
||||
formData.set("fileSize", String(outputStats.size));
|
||||
formData.set("filename", replacementFileName);
|
||||
formData.set("assetData", fileBlob, replacementFileName);
|
||||
if (typeof source.duration === "number") formData.set("duration", String(source.duration));
|
||||
formData.set("isFavorite", source.isFavorite ? "true" : "false");
|
||||
formData.set("assetData", uploadFile, replacementFileName);
|
||||
if (source.visibility) formData.set("visibility", source.visibility);
|
||||
if (source.isFavorite) formData.set("isFavorite", "true");
|
||||
if (Array.isArray(customMetadata) && customMetadata.length > 0) {
|
||||
formData.set("metadata", JSON.stringify(customMetadata.map(({ key, value }: any) => ({ key, value }))));
|
||||
}
|
||||
|
||||
const { ["Content-Type"]: _contentType, ...uploadHeaders } = jsonHeaders;
|
||||
const uploadResponse = await fetch(`${ENV.IMMICH_URL}/api/assets`, {
|
||||
@@ -84,7 +104,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
headers: uploadHeaders,
|
||||
body: formData,
|
||||
});
|
||||
if (!uploadResponse.ok) throw new Error(`Failed to upload compressed asset: ${await readError(uploadResponse)}`);
|
||||
if (!uploadResponse.ok) {
|
||||
const details = await readError(uploadResponse);
|
||||
if (uploadResponse.status === 403) {
|
||||
throw new Error(`Immich rejected the upload (403). The configured API key/session needs asset.upload permission. ${details}`);
|
||||
}
|
||||
throw new Error(`Failed to upload compressed asset (${uploadResponse.status}): ${details}`);
|
||||
}
|
||||
|
||||
const uploaded = await uploadResponse.json();
|
||||
if (!uploaded?.id) throw new Error("Immich upload response did not include an asset id");
|
||||
@@ -95,6 +121,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
newAssetId = uploaded.id;
|
||||
createdNewAsset = true;
|
||||
|
||||
if (Array.isArray(customMetadata) && customMetadata.length > 0) {
|
||||
const metadataItems = customMetadata
|
||||
.filter((item: any) => item?.key && item?.value && typeof item.value === "object")
|
||||
.map(({ key, value }: any) => ({ key, value }));
|
||||
|
||||
if (metadataItems.length > 0) {
|
||||
const customMetadataResponse = await fetch(`${ENV.IMMICH_URL}/api/assets/${newAssetId}/metadata`, {
|
||||
method: "PUT",
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({ items: metadataItems }),
|
||||
});
|
||||
if (!customMetadataResponse.ok) {
|
||||
throw new Error(`Failed to restore custom metadata: ${await readError(customMetadataResponse)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const copyResponse = await fetch(`${ENV.IMMICH_URL}/api/assets/copy`, {
|
||||
method: "PUT",
|
||||
headers: jsonHeaders,
|
||||
@@ -186,6 +229,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
fileName: replacementFileName,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("[compression:replace]", {
|
||||
assetId: dbAsset.id,
|
||||
message: error?.message,
|
||||
stack: error?.stack,
|
||||
});
|
||||
if (rollbackSafe && changedLivePhotoParentIds.length > 0) {
|
||||
await Promise.all(changedLivePhotoParentIds.map((parentId) =>
|
||||
fetch(`${ENV.IMMICH_URL}/api/assets/${parentId}`, {
|
||||
|
||||
Reference in New Issue
Block a user