updated mobile media uploader logic to be more redundant
This commit is contained in:
@@ -12,6 +12,7 @@ import { logDomainEvent } from "@pkg/logger";
|
||||
import type { Context } from "hono";
|
||||
import * as v from "valibot";
|
||||
import { Hono } from "hono";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const mobileController = getMobileController();
|
||||
const fileController = getFileController();
|
||||
@@ -81,6 +82,15 @@ function parseOptionalJsonRecord(value: string | undefined): {
|
||||
}
|
||||
}
|
||||
|
||||
async function makeFileHash(file: File): Promise<string | null> {
|
||||
try {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
return createHash("sha256").update(buffer).digest("hex");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const mobileRouter = new Hono()
|
||||
.post("/register", async (c) => {
|
||||
const fctx = buildFlowExecCtx(c);
|
||||
@@ -348,6 +358,82 @@ export const mobileRouter = new Hono()
|
||||
return respondError(c, fctx, deviceResult.error);
|
||||
}
|
||||
|
||||
const externalMediaId = parsed.output.externalMediaId ?? null;
|
||||
const mediaHash = parsed.output.hash ?? (await makeFileHash(fileEntry));
|
||||
if (externalMediaId) {
|
||||
const existingResult =
|
||||
await mobileController.findMediaAssetByExternalMediaId(
|
||||
fctx,
|
||||
deviceResult.value.id,
|
||||
externalMediaId,
|
||||
);
|
||||
if (existingResult.isErr()) {
|
||||
return respondError(c, fctx, existingResult.error);
|
||||
}
|
||||
|
||||
if (existingResult.value) {
|
||||
logDomainEvent({
|
||||
event: "processor.mobile.media_sync.duplicate_skipped",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: {
|
||||
externalDeviceId,
|
||||
externalMediaId,
|
||||
deviceId: deviceResult.value.id,
|
||||
mediaAssetId: existingResult.value.id,
|
||||
fileId: existingResult.value.fileId,
|
||||
},
|
||||
});
|
||||
return c.json({
|
||||
data: {
|
||||
received: 1,
|
||||
inserted: 0,
|
||||
deviceId: deviceResult.value.id,
|
||||
fileId: existingResult.value.fileId,
|
||||
deduplicated: true,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!externalMediaId && mediaHash) {
|
||||
const existingByHash = await mobileController.findMediaAssetByHash(
|
||||
fctx,
|
||||
deviceResult.value.id,
|
||||
mediaHash,
|
||||
);
|
||||
if (existingByHash.isErr()) {
|
||||
return respondError(c, fctx, existingByHash.error);
|
||||
}
|
||||
|
||||
if (existingByHash.value) {
|
||||
logDomainEvent({
|
||||
event: "processor.mobile.media_sync.duplicate_skipped",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: {
|
||||
externalDeviceId,
|
||||
dedupKey: "hash",
|
||||
mediaHash,
|
||||
deviceId: deviceResult.value.id,
|
||||
mediaAssetId: existingByHash.value.id,
|
||||
fileId: existingByHash.value.fileId,
|
||||
},
|
||||
});
|
||||
return c.json({
|
||||
data: {
|
||||
received: 1,
|
||||
inserted: 0,
|
||||
deviceId: deviceResult.value.id,
|
||||
fileId: existingByHash.value.fileId,
|
||||
deduplicated: true,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const uploadResult = await fileController.uploadFile(
|
||||
fctx,
|
||||
deviceResult.value.ownerUserId,
|
||||
@@ -393,7 +479,7 @@ export const mobileRouter = new Hono()
|
||||
const result = await mobileController.syncMediaByExternalDeviceId(
|
||||
fctx,
|
||||
externalDeviceId,
|
||||
[{ ...parsed.output, fileId: uploadedFileId }],
|
||||
[{ ...parsed.output, hash: mediaHash ?? undefined, fileId: uploadedFileId }],
|
||||
);
|
||||
if (result.isErr()) {
|
||||
await fileController.deleteFiles(
|
||||
@@ -415,6 +501,64 @@ export const mobileRouter = new Hono()
|
||||
return respondError(c, fctx, result.error);
|
||||
}
|
||||
|
||||
if (result.value.inserted === 0) {
|
||||
const rollback = await fileController.deleteFiles(
|
||||
fctx,
|
||||
[uploadedFileId],
|
||||
deviceResult.value.ownerUserId,
|
||||
);
|
||||
if (rollback.isErr()) {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "processor.mobile.media_sync.duplicate_cleanup_failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: rollback.error,
|
||||
meta: {
|
||||
externalDeviceId,
|
||||
fileId: uploadedFileId,
|
||||
externalMediaId,
|
||||
},
|
||||
});
|
||||
return respondError(c, fctx, rollback.error);
|
||||
}
|
||||
|
||||
let dedupedFileId = uploadedFileId;
|
||||
if (externalMediaId) {
|
||||
const existingAfterInsert =
|
||||
await mobileController.findMediaAssetByExternalMediaId(
|
||||
fctx,
|
||||
deviceResult.value.id,
|
||||
externalMediaId,
|
||||
);
|
||||
if (existingAfterInsert.isOk() && existingAfterInsert.value) {
|
||||
dedupedFileId = existingAfterInsert.value.fileId;
|
||||
}
|
||||
}
|
||||
|
||||
logDomainEvent({
|
||||
event: "processor.mobile.media_sync.duplicate_cleaned",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: {
|
||||
externalDeviceId,
|
||||
externalMediaId,
|
||||
deviceId: deviceResult.value.id,
|
||||
uploadedFileId,
|
||||
dedupedFileId,
|
||||
},
|
||||
});
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
...result.value,
|
||||
fileId: dedupedFileId,
|
||||
deduplicated: true,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
|
||||
logDomainEvent({
|
||||
event: "processor.mobile.media_sync.succeeded",
|
||||
fctx,
|
||||
@@ -425,10 +569,12 @@ export const mobileRouter = new Hono()
|
||||
...result.value,
|
||||
},
|
||||
});
|
||||
|
||||
return c.json({
|
||||
data: {
|
||||
...result.value,
|
||||
fileId: uploadedFileId,
|
||||
deduplicated: false,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user