mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
fileshare (#5414)
Co-authored-by: ConnorYoh <[email protected]> Co-authored-by: Connor Yoh <[email protected]> Co-authored-by: EthanHealy01 <[email protected]> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
ConnorYoh
Connor Yoh
EthanHealy01
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
47cad0a131
commit
28613caf8a
@@ -53,9 +53,18 @@ class FileStorageService {
|
||||
type: stirlingFile.type,
|
||||
size: stirlingFile.size,
|
||||
lastModified: stirlingFile.lastModified,
|
||||
createdAt: stub.createdAt,
|
||||
data: arrayBuffer,
|
||||
thumbnail: stub.thumbnailUrl,
|
||||
isLeaf: stub.isLeaf ?? true,
|
||||
remoteStorageId: stub.remoteStorageId,
|
||||
remoteStorageUpdatedAt: stub.remoteStorageUpdatedAt,
|
||||
remoteOwnerUsername: stub.remoteOwnerUsername,
|
||||
remoteOwnedByCurrentUser: stub.remoteOwnedByCurrentUser,
|
||||
remoteAccessRole: stub.remoteAccessRole,
|
||||
remoteSharedViaLink: stub.remoteSharedViaLink,
|
||||
remoteHasShareLinks: stub.remoteHasShareLinks,
|
||||
remoteShareToken: stub.remoteShareToken,
|
||||
|
||||
// History data from stub
|
||||
versionNumber: stub.versionNumber ?? 1,
|
||||
@@ -160,11 +169,19 @@ class FileStorageService {
|
||||
quickKey: record.quickKey,
|
||||
thumbnailUrl: record.thumbnail,
|
||||
isLeaf: record.isLeaf,
|
||||
remoteStorageId: record.remoteStorageId,
|
||||
remoteStorageUpdatedAt: record.remoteStorageUpdatedAt,
|
||||
remoteOwnerUsername: record.remoteOwnerUsername,
|
||||
remoteOwnedByCurrentUser: record.remoteOwnedByCurrentUser,
|
||||
remoteAccessRole: record.remoteAccessRole,
|
||||
remoteSharedViaLink: record.remoteSharedViaLink,
|
||||
remoteHasShareLinks: record.remoteHasShareLinks,
|
||||
remoteShareToken: record.remoteShareToken,
|
||||
versionNumber: record.versionNumber,
|
||||
originalFileId: record.originalFileId,
|
||||
parentFileId: record.parentFileId,
|
||||
toolHistory: record.toolHistory,
|
||||
createdAt: Date.now() // Current session
|
||||
createdAt: record.createdAt || Date.now()
|
||||
};
|
||||
|
||||
resolve(stub);
|
||||
@@ -200,11 +217,19 @@ class FileStorageService {
|
||||
quickKey: record.quickKey,
|
||||
thumbnailUrl: record.thumbnail,
|
||||
isLeaf: record.isLeaf,
|
||||
remoteStorageId: record.remoteStorageId,
|
||||
remoteStorageUpdatedAt: record.remoteStorageUpdatedAt,
|
||||
remoteOwnerUsername: record.remoteOwnerUsername,
|
||||
remoteOwnedByCurrentUser: record.remoteOwnedByCurrentUser,
|
||||
remoteAccessRole: record.remoteAccessRole,
|
||||
remoteSharedViaLink: record.remoteSharedViaLink,
|
||||
remoteHasShareLinks: record.remoteHasShareLinks,
|
||||
remoteShareToken: record.remoteShareToken,
|
||||
versionNumber: record.versionNumber || 1,
|
||||
originalFileId: record.originalFileId || record.id,
|
||||
parentFileId: record.parentFileId,
|
||||
toolHistory: record.toolHistory || [],
|
||||
createdAt: Date.now()
|
||||
createdAt: record.createdAt || Date.now()
|
||||
});
|
||||
}
|
||||
cursor.continue();
|
||||
@@ -215,6 +240,16 @@ class FileStorageService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all history stubs for a given original file ID.
|
||||
*/
|
||||
async getHistoryChainStubs(originalFileId: FileId): Promise<StirlingFileStub[]> {
|
||||
const stubs = await this.getAllStirlingFileStubs();
|
||||
return stubs
|
||||
.filter((stub) => (stub.originalFileId || stub.id) === originalFileId)
|
||||
.sort((a, b) => (a.versionNumber || 1) - (b.versionNumber || 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get leaf StirlingFileStubs only - for unprocessed files
|
||||
*/
|
||||
@@ -243,11 +278,19 @@ class FileStorageService {
|
||||
quickKey: record.quickKey,
|
||||
thumbnailUrl: record.thumbnail,
|
||||
isLeaf: record.isLeaf,
|
||||
remoteStorageId: record.remoteStorageId,
|
||||
remoteStorageUpdatedAt: record.remoteStorageUpdatedAt,
|
||||
remoteOwnerUsername: record.remoteOwnerUsername,
|
||||
remoteOwnedByCurrentUser: record.remoteOwnedByCurrentUser,
|
||||
remoteAccessRole: record.remoteAccessRole,
|
||||
remoteSharedViaLink: record.remoteSharedViaLink,
|
||||
remoteHasShareLinks: record.remoteHasShareLinks,
|
||||
remoteShareToken: record.remoteShareToken,
|
||||
versionNumber: record.versionNumber || 1,
|
||||
originalFileId: record.originalFileId || record.id,
|
||||
parentFileId: record.parentFileId,
|
||||
toolHistory: record.toolHistory || [],
|
||||
createdAt: Date.now()
|
||||
createdAt: record.createdAt || Date.now()
|
||||
});
|
||||
}
|
||||
cursor.continue();
|
||||
@@ -473,6 +516,38 @@ class FileStorageService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update metadata fields for a stored file record.
|
||||
*/
|
||||
async updateFileMetadata(fileId: FileId, updates: Partial<StoredStirlingFileRecord>): Promise<boolean> {
|
||||
try {
|
||||
const db = await this.getDatabase();
|
||||
const transaction = db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const record = await new Promise<StoredStirlingFileRecord | undefined>((resolve, reject) => {
|
||||
const request = store.get(fileId);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result as StoredStirlingFileRecord | undefined);
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updatedRecord = { ...record, ...updates };
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = store.put(updatedRecord);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update file metadata:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import { fileStorage } from '@app/services/fileStorage';
|
||||
import type { FileId, ToolOperation } from '@app/types/file';
|
||||
import type { StirlingFileStub } from '@app/types/fileContext';
|
||||
|
||||
interface ShareBundleEntry {
|
||||
logicalId: string;
|
||||
rootLogicalId: string;
|
||||
parentLogicalId?: string;
|
||||
versionNumber: number;
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
toolHistory?: ToolOperation[];
|
||||
filePath: string;
|
||||
isLeaf: boolean;
|
||||
}
|
||||
|
||||
export interface ShareBundleManifest {
|
||||
schemaVersion: 1;
|
||||
rootLogicalId: string;
|
||||
rootLogicalIds: string[];
|
||||
createdAt: number;
|
||||
entries: ShareBundleEntry[];
|
||||
}
|
||||
|
||||
function sanitizeFilename(name: string): string {
|
||||
const trimmed = name?.trim();
|
||||
if (!trimmed) return 'file';
|
||||
return trimmed.replace(/[\\/:*?"<>|]/g, '_');
|
||||
}
|
||||
|
||||
export async function buildHistoryBundle(originalFileIds: FileId[] | FileId): Promise<{
|
||||
bundleFile: File;
|
||||
manifest: ShareBundleManifest;
|
||||
}> {
|
||||
const roots = Array.isArray(originalFileIds) ? originalFileIds : [originalFileIds];
|
||||
const uniqueRoots = Array.from(new Set(roots));
|
||||
const allStubs: Array<{ rootId: FileId; stubs: Awaited<ReturnType<typeof fileStorage.getHistoryChainStubs>> }> = [];
|
||||
|
||||
for (const rootId of uniqueRoots) {
|
||||
const stubs = await fileStorage.getHistoryChainStubs(rootId);
|
||||
if (stubs.length === 0) {
|
||||
throw new Error('No history chain found for file.');
|
||||
}
|
||||
allStubs.push({ rootId, stubs });
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
const entries: ShareBundleEntry[] = [];
|
||||
|
||||
for (const chain of allStubs) {
|
||||
for (const stub of chain.stubs) {
|
||||
const file = await fileStorage.getStirlingFile(stub.id);
|
||||
if (!file) {
|
||||
throw new Error(`Missing file data for ${stub.name || stub.id}`);
|
||||
}
|
||||
|
||||
const logicalId = stub.id;
|
||||
const filePath = `files/${logicalId}/${sanitizeFilename(stub.name || 'file')}`;
|
||||
const buffer = await file.arrayBuffer();
|
||||
zip.file(filePath, buffer);
|
||||
|
||||
entries.push({
|
||||
logicalId,
|
||||
rootLogicalId: chain.rootId,
|
||||
parentLogicalId: stub.parentFileId,
|
||||
versionNumber: stub.versionNumber || 1,
|
||||
name: stub.name,
|
||||
type: stub.type,
|
||||
size: stub.size,
|
||||
lastModified: stub.lastModified,
|
||||
toolHistory: stub.toolHistory,
|
||||
filePath,
|
||||
isLeaf: Boolean(stub.isLeaf),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: ShareBundleManifest = {
|
||||
schemaVersion: 1,
|
||||
rootLogicalId: uniqueRoots[0],
|
||||
rootLogicalIds: uniqueRoots,
|
||||
createdAt: Date.now(),
|
||||
entries,
|
||||
};
|
||||
|
||||
zip.file('stirling-share.json', JSON.stringify(manifest, null, 2));
|
||||
|
||||
const zipBlob = await zip.generateAsync({
|
||||
type: 'blob',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 },
|
||||
});
|
||||
|
||||
const firstStubName = allStubs[0]?.stubs[0]?.name || 'shared';
|
||||
const rootName = sanitizeFilename(firstStubName);
|
||||
const bundleFile = new File([zipBlob], `${rootName}-history.zip`, {
|
||||
type: 'application/zip',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
|
||||
return { bundleFile, manifest };
|
||||
}
|
||||
|
||||
export async function buildSharePackage(
|
||||
stubs: StirlingFileStub[]
|
||||
): Promise<{
|
||||
bundleFile: File;
|
||||
manifest: ShareBundleManifest;
|
||||
}> {
|
||||
if (stubs.length === 0) {
|
||||
throw new Error('No files provided for sharing.');
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
const entries: ShareBundleEntry[] = [];
|
||||
|
||||
for (const stub of stubs) {
|
||||
const file = await fileStorage.getStirlingFile(stub.id as FileId);
|
||||
if (!file) {
|
||||
throw new Error(`Missing file data for ${stub.name || stub.id}`);
|
||||
}
|
||||
|
||||
const logicalId = stub.id as string;
|
||||
const filePath = `files/${logicalId}/${sanitizeFilename(stub.name || 'file')}`;
|
||||
const buffer = await file.arrayBuffer();
|
||||
zip.file(filePath, buffer);
|
||||
|
||||
entries.push({
|
||||
logicalId,
|
||||
rootLogicalId: logicalId,
|
||||
versionNumber: stub.versionNumber || 1,
|
||||
name: stub.name,
|
||||
type: stub.type,
|
||||
size: stub.size,
|
||||
lastModified: stub.lastModified,
|
||||
toolHistory: stub.toolHistory,
|
||||
filePath,
|
||||
isLeaf: true,
|
||||
});
|
||||
}
|
||||
|
||||
const rootLogicalIds = entries.map((entry) => entry.logicalId);
|
||||
const manifest: ShareBundleManifest = {
|
||||
schemaVersion: 1,
|
||||
rootLogicalId: rootLogicalIds[0],
|
||||
rootLogicalIds,
|
||||
createdAt: Date.now(),
|
||||
entries,
|
||||
};
|
||||
|
||||
zip.file('stirling-share.json', JSON.stringify(manifest, null, 2));
|
||||
|
||||
const zipBlob = await zip.generateAsync({
|
||||
type: 'blob',
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: 6 },
|
||||
});
|
||||
|
||||
const bundleFile = new File([zipBlob], `shared-files.zip`, {
|
||||
type: 'application/zip',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
|
||||
return { bundleFile, manifest };
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import apiClient from '@app/services/apiClient';
|
||||
import { fileStorage } from '@app/services/fileStorage';
|
||||
import { buildHistoryBundle, buildSharePackage } from '@app/services/serverStorageBundle';
|
||||
import type { FileId } from '@app/types/file';
|
||||
import type { StirlingFileStub } from '@app/types/fileContext';
|
||||
|
||||
function resolveUpdatedAt(value: unknown): number {
|
||||
if (!value) {
|
||||
return Date.now();
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? value : Date.now();
|
||||
}
|
||||
const parsed = new Date(String(value)).getTime();
|
||||
return Number.isFinite(parsed) ? parsed : Date.now();
|
||||
}
|
||||
|
||||
export async function uploadHistoryChain(
|
||||
originalFileId: FileId,
|
||||
existingRemoteId?: number
|
||||
): Promise<{ remoteId: number; updatedAt: number; chain: StirlingFileStub[] }> {
|
||||
const chain = await fileStorage.getHistoryChainStubs(originalFileId);
|
||||
if (chain.length === 0) {
|
||||
throw new Error('No history chain found.');
|
||||
}
|
||||
|
||||
const finalStub =
|
||||
chain.slice().reverse().find((stub) => stub.isLeaf !== false) || chain[chain.length - 1];
|
||||
const finalFile = await fileStorage.getStirlingFile(finalStub.id);
|
||||
if (!finalFile) {
|
||||
throw new Error('Missing final file data for sharing.');
|
||||
}
|
||||
|
||||
const { bundleFile, manifest } = await buildHistoryBundle(originalFileId);
|
||||
const auditLog = new File([JSON.stringify(manifest, null, 2)], 'audit-log.json', {
|
||||
type: 'application/json',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append('file', finalFile, finalFile.name);
|
||||
formData.append('historyBundle', bundleFile, bundleFile.name);
|
||||
formData.append('auditLog', auditLog, auditLog.name);
|
||||
|
||||
if (existingRemoteId) {
|
||||
const response = await apiClient.put(`/api/v1/storage/files/${existingRemoteId}`, formData);
|
||||
const updatedAt = resolveUpdatedAt(response.data?.updatedAt);
|
||||
return { remoteId: existingRemoteId, updatedAt, chain };
|
||||
}
|
||||
|
||||
const response = await apiClient.post('/api/v1/storage/files', formData);
|
||||
const remoteId = response.data?.id as number | undefined;
|
||||
if (!remoteId) {
|
||||
throw new Error('Missing stored file ID in response.');
|
||||
}
|
||||
|
||||
const updatedAt = resolveUpdatedAt(response.data?.updatedAt);
|
||||
return { remoteId, updatedAt, chain };
|
||||
}
|
||||
|
||||
export async function uploadHistoryChains(
|
||||
originalFileIds: FileId[],
|
||||
existingRemoteId?: number
|
||||
): Promise<{ remoteId: number; updatedAt: number; chain: StirlingFileStub[] }> {
|
||||
const uniqueRoots = Array.from(new Set(originalFileIds));
|
||||
const chainMap = new Map<FileId, StirlingFileStub[]>();
|
||||
const combinedChain: StirlingFileStub[] = [];
|
||||
const seenIds = new Set<FileId>();
|
||||
const leafStubs: StirlingFileStub[] = [];
|
||||
|
||||
for (const rootId of uniqueRoots) {
|
||||
const chain = await fileStorage.getHistoryChainStubs(rootId);
|
||||
if (chain.length === 0) {
|
||||
throw new Error('No history chain found.');
|
||||
}
|
||||
chainMap.set(rootId, chain);
|
||||
const finalStub =
|
||||
chain.slice().reverse().find((stub) => stub.isLeaf !== false) || chain[chain.length - 1];
|
||||
if (finalStub) {
|
||||
leafStubs.push(finalStub);
|
||||
}
|
||||
for (const stub of chain) {
|
||||
if (!seenIds.has(stub.id as FileId)) {
|
||||
seenIds.add(stub.id as FileId);
|
||||
combinedChain.push(stub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shareFile: File;
|
||||
if (leafStubs.length === 1) {
|
||||
const finalFile = await fileStorage.getStirlingFile(leafStubs[0].id);
|
||||
if (!finalFile) {
|
||||
throw new Error('Missing final file data for sharing.');
|
||||
}
|
||||
shareFile = finalFile;
|
||||
} else {
|
||||
const { bundleFile } = await buildSharePackage(leafStubs);
|
||||
shareFile = bundleFile;
|
||||
}
|
||||
|
||||
const { bundleFile, manifest } = await buildHistoryBundle(uniqueRoots);
|
||||
const auditLog = new File([JSON.stringify(manifest, null, 2)], 'audit-log.json', {
|
||||
type: 'application/json',
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
const formData = new FormData();
|
||||
formData.append('file', shareFile, shareFile.name);
|
||||
formData.append('historyBundle', bundleFile, bundleFile.name);
|
||||
formData.append('auditLog', auditLog, auditLog.name);
|
||||
|
||||
if (existingRemoteId) {
|
||||
const response = await apiClient.put(`/api/v1/storage/files/${existingRemoteId}`, formData);
|
||||
const updatedAt = resolveUpdatedAt(response.data?.updatedAt);
|
||||
return { remoteId: existingRemoteId, updatedAt, chain: combinedChain };
|
||||
}
|
||||
|
||||
const response = await apiClient.post('/api/v1/storage/files', formData);
|
||||
const remoteId = response.data?.id as number | undefined;
|
||||
if (!remoteId) {
|
||||
throw new Error('Missing stored file ID in response.');
|
||||
}
|
||||
|
||||
const updatedAt = resolveUpdatedAt(response.data?.updatedAt);
|
||||
return { remoteId, updatedAt, chain: combinedChain };
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import type { ShareBundleManifest } from '@app/services/serverStorageBundle';
|
||||
|
||||
const MANIFEST_FILENAME = 'stirling-share.json';
|
||||
|
||||
export function parseContentDispositionFilename(disposition?: string): string | null {
|
||||
if (!disposition) return null;
|
||||
const filenameMatch = /filename="([^"]+)"/i.exec(disposition);
|
||||
if (filenameMatch?.[1]) return filenameMatch[1];
|
||||
const utf8Match = /filename\*=UTF-8''([^;]+)/i.exec(disposition);
|
||||
if (utf8Match?.[1]) {
|
||||
try {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
} catch {
|
||||
return utf8Match[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isZipBundle(contentType: string, filename: string): boolean {
|
||||
return contentType.includes('zip') || filename.toLowerCase().endsWith('.zip');
|
||||
}
|
||||
|
||||
export function getShareBundleEntryRootId(
|
||||
manifest: ShareBundleManifest,
|
||||
entry: ShareBundleManifest['entries'][number]
|
||||
): string {
|
||||
return entry.rootLogicalId || manifest.rootLogicalId;
|
||||
}
|
||||
|
||||
export function resolveShareBundleOrder(manifest: ShareBundleManifest): {
|
||||
rootOrder: string[];
|
||||
sortedEntries: ShareBundleManifest['entries'];
|
||||
} {
|
||||
const entryRootId = (entry: ShareBundleManifest['entries'][number]) =>
|
||||
getShareBundleEntryRootId(manifest, entry);
|
||||
const rootOrder =
|
||||
manifest.rootLogicalIds && manifest.rootLogicalIds.length > 0
|
||||
? manifest.rootLogicalIds
|
||||
: Array.from(new Set(manifest.entries.map(entryRootId)));
|
||||
const sortedEntries: ShareBundleManifest['entries'] = [];
|
||||
for (const rootId of rootOrder) {
|
||||
const rootEntries = manifest.entries
|
||||
.filter((entry) => entryRootId(entry) === rootId)
|
||||
.sort((a, b) => a.versionNumber - b.versionNumber);
|
||||
sortedEntries.push(...rootEntries);
|
||||
}
|
||||
return { rootOrder, sortedEntries };
|
||||
}
|
||||
|
||||
export async function loadShareBundleEntries(
|
||||
blob: Blob
|
||||
): Promise<{
|
||||
manifest: ShareBundleManifest;
|
||||
rootOrder: string[];
|
||||
sortedEntries: ShareBundleManifest['entries'];
|
||||
files: File[];
|
||||
} | null> {
|
||||
const zip = await JSZip.loadAsync(blob);
|
||||
const manifestEntry = zip.file(MANIFEST_FILENAME);
|
||||
if (!manifestEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const manifestText = await manifestEntry.async('text');
|
||||
const manifest = JSON.parse(manifestText) as ShareBundleManifest;
|
||||
const { rootOrder, sortedEntries } = resolveShareBundleOrder(manifest);
|
||||
|
||||
const files: File[] = [];
|
||||
for (const entry of sortedEntries) {
|
||||
const zipEntry = zip.file(entry.filePath);
|
||||
if (!zipEntry) {
|
||||
throw new Error(`Missing file entry ${entry.filePath}`);
|
||||
}
|
||||
const fileBlob = await zipEntry.async('blob');
|
||||
files.push(
|
||||
new File([fileBlob], entry.name, {
|
||||
type: entry.type,
|
||||
lastModified: entry.lastModified,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { manifest, rootOrder, sortedEntries, files };
|
||||
}
|
||||
|
||||
export async function extractLatestFilesFromBundle(
|
||||
blob: Blob,
|
||||
filename: string,
|
||||
contentType: string
|
||||
): Promise<File[]> {
|
||||
if (!isZipBundle(contentType, filename)) {
|
||||
return [new File([blob], filename, { type: contentType || blob.type })];
|
||||
}
|
||||
|
||||
const bundle = await loadShareBundleEntries(blob);
|
||||
if (!bundle) {
|
||||
return [new File([blob], filename, { type: contentType || blob.type })];
|
||||
}
|
||||
|
||||
const { manifest, rootOrder, sortedEntries, files } = bundle;
|
||||
const latestByRoot = new Map<string, File>();
|
||||
for (let i = 0; i < sortedEntries.length; i += 1) {
|
||||
const entry = sortedEntries[i];
|
||||
latestByRoot.set(getShareBundleEntryRootId(manifest, entry), files[i]);
|
||||
}
|
||||
|
||||
const latestFiles = rootOrder
|
||||
.map((rootId) => latestByRoot.get(rootId))
|
||||
.filter((file): file is File => Boolean(file));
|
||||
|
||||
if (latestFiles.length > 0) {
|
||||
return latestFiles;
|
||||
}
|
||||
|
||||
return [new File([blob], filename, { type: contentType || blob.type })];
|
||||
}
|
||||
Reference in New Issue
Block a user