-
Notifications
You must be signed in to change notification settings - Fork 6
feat: enhance file download and caching mechanisms #404
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f8371d
481a78d
3d88862
cfbb7eb
7734a38
ff9bb2b
dcf7f24
39c8157
7591ae6
888f290
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { BLOCK_SIZE, PREFETCH_BLOCKS_AHEAD } from './constants'; | ||
|
|
||
| describe('download cache constants', () => { | ||
| it('keeps the built-in defaults', () => { | ||
| expect(BLOCK_SIZE).toBe(4 * 1024 * 1024); | ||
| expect(PREFETCH_BLOCKS_AHEAD).toBe(3); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| /** | ||
| * 4MB blocks — matches the chunk size used by the legacy downloader, proven to work well | ||
| * for this codebase. Each block is downloaded in full on first access regardless of how | ||
| * small the FUSE read is, so subsequent reads within the same block are served from disk. | ||
| * 4MB default blocks — lower latency on slow links while preserving cache locality. | ||
| * Each block is downloaded in full on first access regardless of how small the FUSE read is, | ||
| * so subsequent reads within the same block are served from disk. | ||
| */ | ||
| export const BLOCK_SIZE = 4 * 1024 * 1024; | ||
| const DEFAULT_BLOCK_SIZE_MB = 4; | ||
| export const PREFETCH_BLOCKS_AHEAD = 3; | ||
|
|
||
| export const BLOCK_SIZE = DEFAULT_BLOCK_SIZE_MB * 1024 * 1024; | ||
| export const BITS_PER_BYTE = 8; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ export async function downloadAndCacheBlock({ | |
| blockLength, | ||
| }: Props): Promise<Result<void, Error>> { | ||
| if (isAborted(state)) return { data: undefined }; | ||
| if (blockLength <= 0 || blockStart >= virtualFile.size) return { data: undefined }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something that can happen? meaning: Fuse can ask for a block that is negative?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not just to validate negative ranges; it's also to prevent readings in the 0 range—which fuse does sometimes—and to ensure that invalid ranges aren't attempted, which is rare but does happen. |
||
|
|
||
| try { | ||
| const download = await downloadBlockWithRetry({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,17 @@ | ||
| import { logger } from '@internxt/drive-desktop-core/build/backend'; | ||
| import { type TemporalFile } from '../../../../context/storage/TemporalFiles/domain/TemporalFile'; | ||
| import { type File } from '../../../../context/virtual-drive/files/domain/File'; | ||
| import { | ||
| type FuseError, | ||
| FuseIOError, | ||
| FuseNoSuchFileOrDirectoryError, | ||
| } from '../../../../apps/drive/fuse/callbacks/FuseErrors'; | ||
| import { downloadFileRange } from '../../../../infra/environment/download-file/download-file'; | ||
| import { type FuseError, FuseNoSuchFileOrDirectoryError } from '../../../../apps/drive/fuse/callbacks/FuseErrors'; | ||
| import { type Result } from '../../../../context/shared/domain/Result'; | ||
| import { readChunkFromDisk } from './read-chunk-from-disk'; | ||
| import nodePath from 'node:path'; | ||
| import { PATHS } from '../../../../core/electron/paths'; | ||
| import { EMPTY } from './constants'; | ||
| import { PREFETCH_BLOCKS_AHEAD } from './download-cache/constants'; | ||
| import { readOrHydrate } from './read-or-hydrate'; | ||
| import { type HandleReadDeps, type ReadRange } from './types'; | ||
| import { isThumbnailProcess } from './thumbnail-processes'; | ||
|
|
||
| export type HandleReadCallbackProps = HandleReadDeps & { | ||
| findVirtualFile: (path: string) => Promise<File | undefined>; | ||
| findTemporalFile: (path: string) => Promise<TemporalFile | undefined>; | ||
|
|
@@ -50,11 +47,24 @@ export async function handleReadCallback({ | |
|
|
||
| if (isThumbnailProcess(processName)) { | ||
| logger.debug({ | ||
| msg: '[ReadCallback] thumbnail process, downloading exact range', | ||
| msg: '[ReadCallback] thumbnail process, reading through cache hydration', | ||
| process: processName, | ||
| file: virtualFile.nameWithExtension, | ||
| }); | ||
| return readExactRangeForThumbnail({ bucketId, mnemonic, network, virtualFile, range }); | ||
|
|
||
| const filePath = nodePath.join(PATHS.DOWNLOADED, virtualFile.contentsId); | ||
| return readOrHydrate({ | ||
| bucketId, | ||
| mnemonic, | ||
| network, | ||
| // Thumbnail reads should not spam progress updates in UI. | ||
| onDownloadProgress: () => undefined, | ||
| // Thumbnail reads should not register files as offline available. | ||
| saveToRepository: async () => undefined, | ||
|
Comment on lines
+60
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? imagine a file gets fully hydrated because a thumbnail call, why not save locally? same goes for progress
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could do that, but the goal of this path is to keep the thumbnails' behavior as lightweight as possible. If a thumbnail hydrates the file, it could indeed be saved locally, but that would also trigger additional side effects, such as progress tracking and offline availability logging. To avoid clutter and keep the flow as isolated as possible, for now I prefer that this scenario not alter the cache state or the UI. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but then a file would be downloaded 2 times no? |
||
| virtualFile, | ||
| filePath, | ||
| range, | ||
| }); | ||
| } | ||
|
|
||
| const filePath = nodePath.join(PATHS.DOWNLOADED, virtualFile.contentsId); | ||
|
|
@@ -68,6 +78,7 @@ export async function handleReadCallback({ | |
| virtualFile, | ||
| filePath, | ||
| range, | ||
| prefetchBlocksAhead: PREFETCH_BLOCKS_AHEAD, | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -87,27 +98,3 @@ async function readFromTemporalFile( | |
| const chunk = await readChunkFromDisk(temporalFile.contentFilePath, length, position); | ||
| return { data: chunk ?? EMPTY }; | ||
| } | ||
|
|
||
| type ThumbnailRangeProps = Pick<HandleReadCallbackProps, 'bucketId' | 'mnemonic' | 'network' | 'range'> & { | ||
| virtualFile: File; | ||
| }; | ||
|
|
||
| async function readExactRangeForThumbnail({ | ||
| bucketId, | ||
| mnemonic, | ||
| network, | ||
| virtualFile, | ||
| range, | ||
| }: ThumbnailRangeProps): Promise<Result<Buffer, FuseError>> { | ||
| const { signal } = new AbortController(); | ||
| const result = await downloadFileRange({ | ||
| fileId: virtualFile.contentsId, | ||
| bucketId, | ||
| mnemonic, | ||
| network, | ||
| range, | ||
| signal, | ||
| }); | ||
| if (result.error) return { error: new FuseIOError(result.error.message) }; | ||
| return { data: result.data }; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.