파일 생성 시각 및 파일 마지막 수정 시각을 저장하도록 변경

파일 마지막 수정 시각은 반드시 지정되어야 하며, 파일 시스템에서 읽어옵니다. 파일 생성 시각은 선택적으로 지정될 수 있으며, 이미지일 경우 EXIF에서 추출을 시도합니다. 두 값 모두 클라이언트에서 암호화되어 서버에 저장됩니다.
This commit is contained in:
static
2025-01-13 07:06:31 +09:00
parent 8a620fac78
commit f914026922
14 changed files with 145 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import type { FileInfo } from "$lib/stores";
import { formatDate } from "./service";
import type { SelectedDirectoryEntry } from "../service";
import IconDraft from "~icons/material-symbols/draft";
@@ -34,14 +35,17 @@
{#if $info}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div id="button" onclick={openFile} class="h-12 rounded-xl">
<div id="button" onclick={openFile} class="h-14 rounded-xl">
<div id="button-content" class="flex h-full items-center gap-x-4 p-2 transition">
<div class="flex-shrink-0 text-lg">
<IconDraft class="text-blue-400" />
</div>
<p title={$info.name} class="flex-grow truncate font-medium">
{$info.name}
</p>
<div class="flex flex-grow flex-col overflow-hidden">
<p title={$info.name} class="truncate font-medium">
{$info.name}
</p>
<p class="text-xs text-gray-800">{formatDate($info.createdAt ?? $info.lastModifiedAt)}</p>
</div>
<button
id="open-menu"
onclick={openMenu}

View File

@@ -36,7 +36,7 @@
{#if $info}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div id="button" onclick={openDirectory} class="h-12 rounded-xl">
<div id="button" onclick={openDirectory} class="h-14 rounded-xl">
<div id="button-content" class="flex h-full items-center gap-x-4 p-2 transition">
<div class="flex-shrink-0 text-lg">
<IconFolder />

View File

@@ -28,3 +28,15 @@ export const sortEntries = <T extends DirectoryInfo | FileInfo>(
entries.sort((a, b) => sortFunc(get(a), get(b)));
};
const pad2 = (num: number) => num.toString().padStart(2, "0");
export const formatDate = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
return `${year}. ${month}. ${day}. ${pad2(hours)}:${pad2(minutes)}`;
};

View File

@@ -1,3 +1,4 @@
import ExifReader from "exifreader";
import { callGetApi, callPostApi } from "$lib/hooks";
import { storeHmacSecrets } from "$lib/indexedDB";
import {
@@ -82,6 +83,32 @@ export const requestDuplicateFileScan = async (file: File, hmacSecret: HmacSecre
};
};
const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
const exif = ExifReader.load(fileBuffer);
const dateTimeOriginal = exif["DateTimeOriginal"]?.description;
const offsetTimeOriginal = exif["OffsetTimeOriginal"]?.description;
if (!dateTimeOriginal) return undefined;
const [date, time] = dateTimeOriginal.split(" ");
if (!date || !time) return undefined;
const [year, month, day] = date.split(":").map(Number);
const [hour, minute, second] = time.split(":").map(Number);
if (!year || !month || !day || !hour || !minute || !second) return undefined;
if (!offsetTimeOriginal) {
// No timezone information -> Local timezone
return new Date(year, month - 1, day, hour, minute, second);
}
const offsetSign = offsetTimeOriginal[0] === "+" ? 1 : -1;
const [offsetHour, offsetMinute] = offsetTimeOriginal.slice(1).split(":").map(Number);
const utcDate = Date.UTC(year, month - 1, day, hour, minute, second);
const offsetMs = offsetSign * ((offsetHour ?? 0) * 60 + (offsetMinute ?? 0)) * 60 * 1000;
return new Date(utcDate - offsetMs);
};
export const requestFileUpload = async (
file: File,
fileBuffer: ArrayBuffer,
@@ -90,9 +117,17 @@ export const requestFileUpload = async (
masterKey: MasterKey,
hmacSecret: HmacSecret,
) => {
let createdAt = undefined;
if (file.type.startsWith("image/")) {
createdAt = extractExifDateTime(fileBuffer);
}
const { dataKey, dataKeyVersion } = await generateDataKey();
const nameEncrypted = await encryptString(file.name, dataKey);
const fileEncrypted = await encryptData(fileBuffer, dataKey);
const nameEncrypted = await encryptString(file.name, dataKey);
const createdAtEncrypted =
createdAt && (await encryptString(createdAt.getTime().toString(), dataKey));
const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey);
const form = new FormData();
form.set(
@@ -108,6 +143,10 @@ export const requestFileUpload = async (
contentIv: fileEncrypted.iv,
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
createdAt: createdAtEncrypted?.ciphertext,
createdAtIv: createdAtEncrypted?.iv,
lastModifiedAt: lastModifiedAtEncrypted.ciphertext,
lastModifiedAtIv: lastModifiedAtEncrypted.iv,
} satisfies FileUploadRequest),
);
form.set("content", new Blob([fileEncrypted.ciphertext]));

View File

@@ -16,8 +16,16 @@ export const GET: RequestHandler = async ({ locals, params }) => {
if (!zodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data;
const { mekVersion, encDek, dekVersion, contentType, encContentIv, encName } =
await getFileInformation(userId, id);
const {
mekVersion,
encDek,
dekVersion,
contentType,
encContentIv,
encName,
encCreatedAt,
encLastModifiedAt,
} = await getFileInformation(userId, id);
return json(
fileInfoResponse.parse({
mekVersion,
@@ -27,6 +35,10 @@ export const GET: RequestHandler = async ({ locals, params }) => {
contentIv: encContentIv,
name: encName.ciphertext,
nameIv: encName.iv,
createdAt: encCreatedAt?.ciphertext,
createdAtIv: encCreatedAt?.iv,
lastModifiedAt: encLastModifiedAt.ciphertext,
lastModifiedAtIv: encLastModifiedAt.iv,
} satisfies FileInfoResponse),
);
};

View File

@@ -27,7 +27,13 @@ export const POST: RequestHandler = async ({ locals, request }) => {
contentIv,
name,
nameIv,
createdAt,
createdAtIv,
lastModifiedAt,
lastModifiedAtIv,
} = zodRes.data;
if ((createdAt && !createdAtIv) || (!createdAt && createdAtIv))
error(400, "Invalid request body");
await uploadFile(
{
@@ -42,6 +48,10 @@ export const POST: RequestHandler = async ({ locals, request }) => {
encContentIv: contentIv,
encName: name,
encNameIv: nameIv,
encCreatedAt: createdAt ?? null,
encCreatedAtIv: createdAtIv ?? null,
encLastModifiedAt: lastModifiedAt,
encLastModifiedAtIv: lastModifiedAtIv,
},
content.stream(),
);