mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
스트리밍 방식으로 동영상을 불러올 때 다운로드 메뉴가 표시되지 않는 버그 수정
This commit is contained in:
@@ -154,6 +154,7 @@
|
|||||||
? info?.parentId
|
? info?.parentId
|
||||||
: undefined}
|
: undefined}
|
||||||
{fileBlob}
|
{fileBlob}
|
||||||
|
downloadUrl={videoStreamUrl}
|
||||||
filename={info?.name}
|
filename={info?.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,17 +10,29 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
directoryId?: "root" | number;
|
directoryId?: "root" | number;
|
||||||
|
downloadUrl?: string;
|
||||||
fileBlob?: Blob;
|
fileBlob?: Blob;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { directoryId, fileBlob, filename, isOpen = $bindable() }: Props = $props();
|
let { directoryId, downloadUrl, fileBlob, filename, isOpen = $bindable() }: Props = $props();
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (fileBlob && filename) {
|
||||||
|
FileSaver.saveAs(fileBlob, filename);
|
||||||
|
} else if (downloadUrl && filename) {
|
||||||
|
// Use streaming download via Content-Disposition header
|
||||||
|
const url = new URL(downloadUrl, window.location.origin);
|
||||||
|
url.searchParams.set("download", filename);
|
||||||
|
window.open(url.toString(), "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onclick={() => (isOpen = false)} />
|
<svelte:window onclick={() => (isOpen = false)} />
|
||||||
|
|
||||||
{#if isOpen && (directoryId || fileBlob)}
|
{#if isOpen && (directoryId || downloadUrl || fileBlob)}
|
||||||
<div
|
<div
|
||||||
class="absolute right-2 top-full z-20 space-y-1 rounded-lg bg-white px-1 py-2 shadow-2xl"
|
class="absolute right-2 top-full z-20 space-y-1 rounded-lg bg-white px-1 py-2 shadow-2xl"
|
||||||
transition:fly={{ y: -8, duration: 200 }}
|
transition:fly={{ y: -8, duration: 200 }}
|
||||||
@@ -49,10 +61,8 @@
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
{/if}
|
{/if}
|
||||||
{#if fileBlob}
|
{#if fileBlob || downloadUrl}
|
||||||
{@render menuButton(IconCloudDownload, "다운로드", () => {
|
{@render menuButton(IconCloudDownload, "다운로드", handleDownload)}
|
||||||
FileSaver.saveAs(fileBlob, filename);
|
|
||||||
})}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,15 +10,23 @@ const createResponse = (
|
|||||||
isRangeRequest: boolean,
|
isRangeRequest: boolean,
|
||||||
range: { start: number; end: number; total: number },
|
range: { start: number; end: number; total: number },
|
||||||
contentType?: string,
|
contentType?: string,
|
||||||
|
downloadFilename?: string,
|
||||||
) => {
|
) => {
|
||||||
return new Response(stream, {
|
const headers: Record<string, string> = {
|
||||||
status: isRangeRequest ? 206 : 200,
|
|
||||||
headers: {
|
|
||||||
"Accept-Ranges": "bytes",
|
"Accept-Ranges": "bytes",
|
||||||
"Content-Length": String(range.end - range.start + 1),
|
"Content-Length": String(range.end - range.start + 1),
|
||||||
"Content-Type": contentType ?? "application/octet-stream",
|
"Content-Type": contentType ?? "application/octet-stream",
|
||||||
...(isRangeRequest ? getContentRangeHeader(range) : {}),
|
...(isRangeRequest ? getContentRangeHeader(range) : {}),
|
||||||
},
|
};
|
||||||
|
|
||||||
|
if (downloadFilename) {
|
||||||
|
headers["Content-Disposition"] =
|
||||||
|
`attachment; filename*=UTF-8''${encodeURIComponent(downloadFilename)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
status: isRangeRequest ? 206 : 200,
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,6 +34,7 @@ const streamFromOpfs = async (
|
|||||||
file: File,
|
file: File,
|
||||||
metadata?: FileMetadata,
|
metadata?: FileMetadata,
|
||||||
range?: { start?: number; end?: number },
|
range?: { start?: number; end?: number },
|
||||||
|
downloadFilename?: string,
|
||||||
) => {
|
) => {
|
||||||
const start = range?.start ?? 0;
|
const start = range?.start ?? 0;
|
||||||
const end = range?.end ?? file.size - 1;
|
const end = range?.end ?? file.size - 1;
|
||||||
@@ -38,6 +47,7 @@ const streamFromOpfs = async (
|
|||||||
!!range,
|
!!range,
|
||||||
{ start, end, total: file.size },
|
{ start, end, total: file.size },
|
||||||
metadata?.contentType,
|
metadata?.contentType,
|
||||||
|
downloadFilename,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +55,7 @@ const streamFromServer = async (
|
|||||||
id: number,
|
id: number,
|
||||||
metadata: FileMetadata,
|
metadata: FileMetadata,
|
||||||
range?: { start?: number; end?: number },
|
range?: { start?: number; end?: number },
|
||||||
|
downloadFilename?: string,
|
||||||
) => {
|
) => {
|
||||||
const totalSize = getDecryptedSize(metadata.encContentSize, metadata.isLegacy);
|
const totalSize = getDecryptedSize(metadata.encContentSize, metadata.isLegacy);
|
||||||
const start = range?.start ?? 0;
|
const start = range?.start ?? 0;
|
||||||
@@ -59,40 +70,64 @@ const streamFromServer = async (
|
|||||||
const apiResponse = await fetch(`/api/file/${id}/download`, {
|
const apiResponse = await fetch(`/api/file/${id}/download`, {
|
||||||
headers: { Range: `bytes=${encryptedRange.start}-${encryptedRange.end}` },
|
headers: { Range: `bytes=${encryptedRange.start}-${encryptedRange.end}` },
|
||||||
});
|
});
|
||||||
if (apiResponse.status !== 206) {
|
if (apiResponse.status !== 206 || !apiResponse.body) {
|
||||||
return new Response("Failed to fetch encrypted file", { status: 502 });
|
return new Response("Failed to fetch encrypted file", { status: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata.isLegacy) {
|
||||||
const fileEncrypted = await apiResponse.arrayBuffer();
|
const fileEncrypted = await apiResponse.arrayBuffer();
|
||||||
|
const decrypted = await decryptChunk(fileEncrypted, metadata.dataKey);
|
||||||
return createResponse(
|
return createResponse(
|
||||||
new ReadableStream<Uint8Array>({
|
new ReadableStream<Uint8Array>({
|
||||||
async start(controller) {
|
start(controller) {
|
||||||
if (metadata.isLegacy) {
|
|
||||||
const decrypted = await decryptChunk(fileEncrypted, metadata.dataKey);
|
|
||||||
controller.enqueue(new Uint8Array(decrypted.slice(start, end + 1)));
|
controller.enqueue(new Uint8Array(decrypted.slice(start, end + 1)));
|
||||||
controller.close();
|
controller.close();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks = encryptedRange.lastChunkIndex - encryptedRange.firstChunkIndex + 1;
|
|
||||||
|
|
||||||
for (let i = 0; i < chunks; i++) {
|
|
||||||
const chunk = await decryptChunk(
|
|
||||||
fileEncrypted.slice(i * ENCRYPTED_CHUNK_SIZE, (i + 1) * ENCRYPTED_CHUNK_SIZE),
|
|
||||||
metadata.dataKey,
|
|
||||||
);
|
|
||||||
const sliceStart = i === 0 ? start % CHUNK_SIZE : 0;
|
|
||||||
const sliceEnd = i === chunks - 1 ? (end % CHUNK_SIZE) + 1 : chunk.byteLength;
|
|
||||||
controller.enqueue(new Uint8Array(chunk.slice(sliceStart, sliceEnd)));
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.close();
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
!!range,
|
!!range,
|
||||||
{ start, end, total: totalSize },
|
{ start, end, total: totalSize },
|
||||||
metadata.contentType,
|
metadata.contentType,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalChunks = encryptedRange.lastChunkIndex - encryptedRange.firstChunkIndex + 1;
|
||||||
|
let currentChunkIndex = 0;
|
||||||
|
let buffer = new Uint8Array(0);
|
||||||
|
|
||||||
|
const decryptingStream = new TransformStream<Uint8Array, Uint8Array>({
|
||||||
|
async transform(chunk, controller) {
|
||||||
|
const newBuffer = new Uint8Array(buffer.length + chunk.length);
|
||||||
|
newBuffer.set(buffer);
|
||||||
|
newBuffer.set(chunk, buffer.length);
|
||||||
|
buffer = newBuffer;
|
||||||
|
|
||||||
|
while (buffer.length >= ENCRYPTED_CHUNK_SIZE && currentChunkIndex < totalChunks - 1) {
|
||||||
|
const encryptedChunk = buffer.slice(0, ENCRYPTED_CHUNK_SIZE);
|
||||||
|
buffer = buffer.slice(ENCRYPTED_CHUNK_SIZE);
|
||||||
|
|
||||||
|
const decrypted = await decryptChunk(encryptedChunk.buffer, metadata.dataKey);
|
||||||
|
const sliceStart = currentChunkIndex === 0 ? start % CHUNK_SIZE : 0;
|
||||||
|
controller.enqueue(new Uint8Array(decrypted.slice(sliceStart)));
|
||||||
|
currentChunkIndex++;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async flush(controller) {
|
||||||
|
if (buffer.length > 0) {
|
||||||
|
const decrypted = await decryptChunk(buffer.buffer, metadata.dataKey);
|
||||||
|
const sliceStart = currentChunkIndex === 0 ? start % CHUNK_SIZE : 0;
|
||||||
|
const sliceEnd = (end % CHUNK_SIZE) + 1;
|
||||||
|
controller.enqueue(new Uint8Array(decrypted.slice(sliceStart, sliceEnd)));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return createResponse(
|
||||||
|
apiResponse.body.pipeThrough(decryptingStream),
|
||||||
|
!!range,
|
||||||
|
{ start, end, total: totalSize },
|
||||||
|
metadata.contentType,
|
||||||
|
downloadFilename,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptFileHandler = async (request: Request) => {
|
const decryptFileHandler = async (request: Request) => {
|
||||||
@@ -102,13 +137,14 @@ const decryptFileHandler = async (request: Request) => {
|
|||||||
throw new Response("Invalid file id", { status: 400 });
|
throw new Response("Invalid file id", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadFilename = url.searchParams.get("download") ?? undefined;
|
||||||
const metadata = fileMetadataStore.get(fileId);
|
const metadata = fileMetadataStore.get(fileId);
|
||||||
const range = parseRangeHeader(request.headers.get("Range"));
|
const range = parseRangeHeader(request.headers.get("Range"));
|
||||||
const cache = await getFile(`/cache/${fileId}`);
|
const cache = await getFile(`/cache/${fileId}`);
|
||||||
if (cache) {
|
if (cache) {
|
||||||
return streamFromOpfs(cache, metadata, range);
|
return streamFromOpfs(cache, metadata, range, downloadFilename);
|
||||||
} else if (metadata) {
|
} else if (metadata) {
|
||||||
return streamFromServer(fileId, metadata, range);
|
return streamFromServer(fileId, metadata, range, downloadFilename);
|
||||||
} else {
|
} else {
|
||||||
return new Response("Decryption not prepared", { status: 400 });
|
return new Response("Decryption not prepared", { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user