Skip to content

Commit

Permalink
[MS] Adds download file feature in web mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Max-7 committed Jan 22, 2025
1 parent a413673 commit b8d05fb
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 21 deletions.
73 changes: 73 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"luxon": "^3.4.4",
"megashark-lib": "git+https://github.com/Scille/megashark-lib.git#77e1cde26a5769ce9105687cbd64362ce0056c9c",
"monaco-editor": "^0.52.0",
"native-file-system-adapter": "^3.0.1",
"pdfjs-dist": "^4.8.69",
"qrcode-vue3": "^1.6.8",
"uuid": "^9.0.1",
Expand Down
45 changes: 45 additions & 0 deletions client/src/parsec/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { DateTime } from 'luxon';
const MOCK_OPENED_FILES = new Map<FileDescriptor, FsPath>();
let MOCK_CURRENT_FD = 1;

export const DEFAULT_READ_SIZE = 256_000;

export async function createFile(workspaceHandle: WorkspaceHandle, path: FsPath): Promise<Result<FileID, WorkspaceCreateFileError>> {
if (!needsMocks()) {
return await libparsec.workspaceCreateFile(workspaceHandle, path);
Expand Down Expand Up @@ -238,6 +240,49 @@ export async function parseFileLink(link: string): Promise<Result<ParsedParsecAd
return result as Result<ParsedParsecAddrWorkspacePath, ParseParsecAddrError>;
}

export async function createReadStream(workspaceHandle: WorkspaceHandle, path: FsPath): Promise<ReadableStream> {
let fd: FileDescriptor | undefined;
let offset = 0;

return new ReadableStream({
async start(controller: ReadableStreamDefaultController): Promise<void> {
const result = await openFile(workspaceHandle, path, { read: true });
if (!result.ok) {
return controller.error(result.error);
}
fd = result.value;
},

async pull(controller: ReadableStreamDefaultController): Promise<void> {
if (fd === undefined) {
controller.error('No file descriptor');
return;
}
const result = await readFile(workspaceHandle, fd, offset, DEFAULT_READ_SIZE);

if (!result.ok) {
return controller.error(result.error);
}
if (result.value.length === 0) {
controller.close();
await closeFile(workspaceHandle, fd);
fd = undefined;
} else {
offset += result.value.length;
controller.enqueue(result.value);
}
},

async cancel(): Promise<void> {
if (fd !== undefined) {
await closeFile(workspaceHandle, fd);
}
},

type: 'bytes',
});
}

export async function openFile(
workspaceHandle: WorkspaceHandle,
path: FsPath,
Expand Down
11 changes: 5 additions & 6 deletions client/src/services/fileOperationManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

import {
DEFAULT_READ_SIZE,
EntryStatFile,
EntryTree,
FileDescriptor,
Expand Down Expand Up @@ -409,7 +410,6 @@ class FileOperationManager {

let loop = true;
let offset = 0;
const READ_CHUNK_SIZE = 1_000_000;
while (loop) {
// Check if the copy has been cancelled
let shouldCancel = false;
Expand All @@ -426,7 +426,7 @@ class FileOperationManager {
}

// Read the source
const readResult = await readFile(data.workspaceHandle, fdR, offset, READ_CHUNK_SIZE);
const readResult = await readFile(data.workspaceHandle, fdR, offset, DEFAULT_READ_SIZE);

// Failed to read, cancel the copy
if (!readResult.ok) {
Expand All @@ -440,7 +440,7 @@ class FileOperationManager {
throw Error('Failed to write the destination');
}
// Smaller that what we asked for, we're at the end of the file
if (chunk.byteLength < READ_CHUNK_SIZE) {
if (chunk.byteLength < DEFAULT_READ_SIZE) {
loop = false;
} else {
// Otherwise, move the offset and keep going
Expand Down Expand Up @@ -612,7 +612,6 @@ class FileOperationManager {

let loop = true;
let offset = 0;
const READ_CHUNK_SIZE = 1_000_000;
while (loop) {
// Check if the copy has been cancelled
let shouldCancel = false;
Expand All @@ -629,7 +628,7 @@ class FileOperationManager {
}

// Read the source
const readResult = await readHistoryFile(data.workspaceHandle, fdR, offset, READ_CHUNK_SIZE);
const readResult = await readHistoryFile(data.workspaceHandle, fdR, offset, DEFAULT_READ_SIZE);

// Failed to read, cancel the copy
if (!readResult.ok) {
Expand All @@ -643,7 +642,7 @@ class FileOperationManager {
throw Error('Failed to write the destination');
}
// Smaller that what we asked for, we're at the end of the file
if (chunk.byteLength < READ_CHUNK_SIZE) {
if (chunk.byteLength < DEFAULT_READ_SIZE) {
loop = false;
} else {
// Otherwise, move the offset and keep going
Expand Down
3 changes: 1 addition & 2 deletions client/src/views/files/FileContextMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,9 @@

<ion-item
button
v-if="!multipleFiles && !isDesktop()"
v-if="!multipleFiles && !isDesktop() && isFile"
@click="onClick(FileAction.Download)"
class="ion-no-padding list-group-item"
v-show="false"
>
<ion-icon :icon="download" />
<ion-label class="body list-group-item__label">
Expand Down
21 changes: 20 additions & 1 deletion client/src/views/files/FoldersPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ import { Ref, computed, inject, onMounted, onUnmounted, ref } from 'vue';
import { EntrySyncedData, EventData, EventDistributor, EventDistributorKey, Events } from '@/services/eventDistributor';
import { openPath, showInExplorer } from '@/services/fileOpener';
import { WorkspaceTagRole } from '@/components/workspaces';
import { showSaveFilePicker } from 'native-file-system-adapter';

interface FoldersPageSavedData {
displayState?: DisplayState;
Expand Down Expand Up @@ -1161,7 +1162,25 @@ async function copyEntries(entries: EntryModel[]): Promise<void> {
}

async function downloadEntries(entries: EntryModel[]): Promise<void> {
console.log('Download', entries);
if (entries.length !== 1) {
return;
}
if (!workspaceInfo.value) {
window.electronAPI.log('error', 'No workspace info when trying to download a file');
return;
}

try {
const saveHandle = await showSaveFilePicker({
_preferPolyfill: false,
suggestedName: entries[0].name,
});

const stream = await parsec.createReadStream(workspaceInfo.value.handle, entries[0].path);
await stream.pipeTo(await saveHandle.createWritable());
} catch (e: any) {
window.electronAPI.log('error', `Failed to download file: ${e.toString()}`);
}
}

async function showHistory(entries: EntryModel[]): Promise<void> {
Expand Down
8 changes: 4 additions & 4 deletions client/src/views/viewers/FileViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
openFileAt,
readHistoryFile,
closeHistoryFile,
DEFAULT_READ_SIZE,
} from '@/parsec';
import { IonPage, IonContent, IonButton, IonText, IonIcon, IonButtons, modalController } from '@ionic/vue';
import { informationCircle, open } from 'ionicons/icons';
Expand All @@ -85,7 +86,6 @@ const viewerComponent: Ref<Component | null> = shallowRef(null);
const contentInfo: Ref<FileContentInfo | null> = ref(null);
const detectedFileType = ref<DetectedFileType | null>(null);
const loaded = ref(false);
const READ_CHUNK_SIZE = 512_000;
const atDateTime: Ref<DateTime | null> = ref(null);

onMounted(async () => {
Expand Down Expand Up @@ -160,16 +160,16 @@ onMounted(async () => {
while (loop) {
let readResult;
if (!atDateTime.value) {
readResult = await readFile(workspaceHandle, openResult.value, offset, READ_CHUNK_SIZE);
readResult = await readFile(workspaceHandle, openResult.value, offset, DEFAULT_READ_SIZE);
} else {
readResult = await readHistoryFile(workspaceHandle, openResult.value, offset, READ_CHUNK_SIZE);
readResult = await readHistoryFile(workspaceHandle, openResult.value, offset, DEFAULT_READ_SIZE);
}
if (!readResult.ok) {
throw Error(JSON.stringify(readResult.error));
}
const buffer = new Uint8Array(readResult.value);
contentInfo.value?.data.set(buffer, offset);
if (readResult.value.byteLength < READ_CHUNK_SIZE) {
if (readResult.value.byteLength < DEFAULT_READ_SIZE) {
loop = false;
}
offset += readResult.value.byteLength;
Expand Down
10 changes: 6 additions & 4 deletions client/tests/e2e/specs/document_context_menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ for (const gridMode of [false, true]) {
await expect(documents.locator('.file-context-menu')).toBeVisible();
const popover = documents.locator('.file-context-menu');
await expect(popover.getByRole('group')).toHaveCount(2);
await expect(popover.getByRole('listitem')).toHaveCount(9);
await expect(popover.getByRole('listitem')).toHaveCount(10);
await expect(popover.getByRole('listitem')).toHaveText([
'Manage file',
'Rename',
'Move to',
'Make a copy',
'Delete',
'History',
'Download',
'Details',
'Collaboration',
'Copy link',
Expand All @@ -75,14 +76,15 @@ for (const gridMode of [false, true]) {
await expect(documents.locator('.file-context-menu')).toBeVisible();
const popover = documents.locator('.file-context-menu');
await expect(popover.getByRole('group')).toHaveCount(2);
await expect(popover.getByRole('listitem')).toHaveCount(9);
await expect(popover.getByRole('listitem')).toHaveCount(10);
await expect(popover.getByRole('listitem')).toHaveText([
'Manage file',
'Rename',
'Move to',
'Make a copy',
'Delete',
'History',
'Download',
'Details',
'Collaboration',
'Copy link',
Expand Down Expand Up @@ -203,8 +205,8 @@ for (const gridMode of [false, true]) {
await expect(documentsReadOnly.locator('.file-context-menu')).toBeVisible();
const popover = documentsReadOnly.locator('.file-context-menu');
await expect(popover.getByRole('group')).toHaveCount(2);
await expect(popover.getByRole('listitem')).toHaveCount(4);
await expect(popover.getByRole('listitem')).toHaveText(['Manage file', 'Details', 'Collaboration', 'Copy link']);
await expect(popover.getByRole('listitem')).toHaveCount(5);
await expect(popover.getByRole('listitem')).toHaveText(['Manage file', 'Download', 'Details', 'Collaboration', 'Copy link']);
});

msTest(`Move document in ${gridMode ? 'grid' : 'list'} mode`, async ({ documents }) => {
Expand Down
13 changes: 9 additions & 4 deletions client/tests/e2e/specs/file_details.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ for (const testData of TEST_DATA) {

await files.nth(testData.index).hover();
await files.nth(testData.index).locator('.options-button').click();
expect(connected.locator('.file-context-menu').getByRole('listitem')).toHaveCount(9);
await connected.locator('.file-context-menu').getByRole('listitem').nth(6).click();
if (testData.isFile) {
expect(connected.locator('.file-context-menu').getByRole('listitem')).toHaveCount(10);
await connected.locator('.file-context-menu').getByRole('listitem').nth(7).click();
} else {
expect(connected.locator('.file-context-menu').getByRole('listitem')).toHaveCount(9);
await connected.locator('.file-context-menu').getByRole('listitem').nth(6).click();
}
await expect(connected.locator('.file-details-modal')).toBeVisible();
const modal = connected.locator('.file-details-modal');
await expect(modal.locator('.ms-modal-header__title ')).toHaveText(new RegExp(`^Details on ${nameMatcher}$`));
Expand Down Expand Up @@ -119,8 +124,8 @@ msTest('Show file details in grid mode', async ({ connected }) => {
expect(connected.locator('.file-details-modal')).toBeHidden();
await files.nth(3).hover();
await files.nth(3).locator('.card-option').click();
expect(connected.locator('.file-context-menu').getByRole('listitem')).toHaveCount(9);
await connected.locator('.file-context-menu').getByRole('listitem').nth(6).click();
expect(connected.locator('.file-context-menu').getByRole('listitem')).toHaveCount(10);
await connected.locator('.file-context-menu').getByRole('listitem').nth(7).click();
await expect(connected.locator('.file-details-modal')).toBeVisible();
const modal = connected.locator('.file-details-modal');
await expect(modal.locator('.ms-modal-header__title ')).toHaveText(/^Details on File_[a-z0-9._]+$/);
Expand Down

0 comments on commit b8d05fb

Please sign in to comment.