Skip to content

Commit d5a71fb

Browse files
adds file download feature (#69)
1 parent 9bfb4ba commit d5a71fb

10 files changed

Lines changed: 357 additions & 21 deletions

File tree

frontend/src/pages/EditorPage.tsx

Lines changed: 212 additions & 19 deletions
Large diffs are not rendered by default.

frontend/src/pages/WorkflowEditorPage.css

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,11 @@
250250
display: inline-flex;
251251
}
252252

253+
.wf-tree-mobile-trigger,
254+
.wf-mobile-action-backdrop {
255+
display: none;
256+
}
257+
253258
.wf-tree-actions button {
254259
padding: 2px 6px;
255260
font-size: 0.72rem;
@@ -436,10 +441,80 @@
436441

437442
.wf-tree-row {
438443
min-height: 36px;
444+
align-items: center;
445+
padding-right: 4px;
439446
}
440447

441448
.wf-tree-actions {
449+
display: none;
450+
}
451+
452+
.wf-tree-row:hover .wf-tree-actions {
453+
display: none;
454+
}
455+
456+
.wf-tree-mobile-trigger {
442457
display: inline-flex;
458+
align-items: center;
459+
justify-content: center;
460+
margin-left: auto;
461+
width: 30px;
462+
height: 30px;
463+
border: 1px solid #4a5568;
464+
background: #374151;
465+
color: #e5e7eb;
466+
border-radius: 8px;
467+
cursor: pointer;
468+
flex-shrink: 0;
469+
}
470+
471+
.wf-mobile-action-backdrop {
472+
display: flex;
473+
position: fixed;
474+
inset: 0;
475+
z-index: 1000;
476+
align-items: flex-end;
477+
justify-content: center;
478+
padding: 16px;
479+
background: rgba(3, 7, 18, 0.5);
480+
}
481+
482+
.wf-mobile-action-sheet {
483+
width: min(360px, 100%);
484+
padding: 8px;
485+
border: 1px solid #374151;
486+
border-radius: 12px;
487+
background: #111827;
488+
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.42);
489+
}
490+
491+
.wf-mobile-action-title {
492+
padding: 8px 10px 10px;
493+
color: #d1d5db;
494+
font-size: 0.82rem;
495+
font-weight: 700;
496+
overflow: hidden;
497+
text-overflow: ellipsis;
498+
white-space: nowrap;
499+
}
500+
501+
.wf-mobile-action-sheet button {
502+
width: 100%;
503+
text-align: left;
504+
border: 1px solid #4a5568;
505+
background: #374151;
506+
color: #e5e7eb;
507+
border-radius: 8px;
508+
padding: 10px 12px;
509+
cursor: pointer;
510+
font-size: 0.9rem;
511+
margin-top: 6px;
512+
}
513+
514+
.wf-mobile-action-sheet button.danger {
515+
border-color: #7f1d1d;
516+
background: #450a0a;
517+
color: #fecaca;
443518
}
444519
}
445520

@@ -457,4 +532,8 @@
457532
.wf-tree-actions button {
458533
padding: 0.45rem 0.7rem;
459534
}
535+
536+
.wf-mobile-action-backdrop {
537+
padding: 10px;
538+
}
460539
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,4 @@
8787
"supertest": "^7.2.2",
8888
"ts-prune": "^0.10.3"
8989
}
90-
}
90+
}

src/skills/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { assertCommonPermissionMode } from "../utils/permission-common";
1616
import {
1717
createSkillFileOrFolder,
18+
downloadSkillPath,
1819
deleteSkillPath,
1920
listSkillDirectory,
2021
readSkillFileContent,
@@ -315,6 +316,7 @@ registerResourceFileRoutes({
315316
assertCanEdit: assertCanEditSkillFiles,
316317
listDirectory: listSkillDirectory,
317318
readFileContent: readSkillFileContent,
319+
downloadPath: downloadSkillPath,
318320
saveFileContent: saveSkillFileContent,
319321
createFileOrFolder: createSkillFileOrFolder,
320322
uploadFileFromTempPath: uploadSkillFileFromTempPath,

src/utils/resource-file-routes.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ interface ResourceFileRouteOptions {
2020
assertCanEdit: (slug: string, userId: string) => Promise<void>;
2121
listDirectory: (slug: string, rawPath: string | undefined) => FileTreeResponse;
2222
readFileContent: (slug: string, rawPath: string | undefined) => FileContentResponse;
23+
downloadPath: (slug: string, rawPath: string | undefined) => {
24+
filename: string;
25+
contentType: string;
26+
body: Buffer | NodeJS.ReadableStream;
27+
};
2328
saveFileContent: (slug: string, payload: { path: string; content: string; base_etag: string }) => FileSaveResponse;
2429
createFileOrFolder: (slug: string, parentPath: string, name: string, kind: "file" | "directory") => FileNode;
2530
uploadFileFromTempPath: (slug: string, parentPath: string, name: string, tempFilePath: string) => FileNode;
@@ -37,6 +42,7 @@ export function registerResourceFileRoutes(options: ResourceFileRouteOptions): v
3742
assertCanEdit,
3843
listDirectory,
3944
readFileContent,
45+
downloadPath,
4046
saveFileContent,
4147
createFileOrFolder,
4248
uploadFileFromTempPath,
@@ -72,6 +78,22 @@ export function registerResourceFileRoutes(options: ResourceFileRouteOptions): v
7278
res.json(content);
7379
}, true));
7480

81+
router.get("/:slug/files/download", apiHandler(async (req, res) => {
82+
const slug = req.params.slug as string;
83+
const authReq = req as AuthenticatedRequest;
84+
await assertCanView(slug, authReq.userId!);
85+
await ensureResourceExists(slug);
86+
const rawPath = typeof req.query.path === "string" ? req.query.path : undefined;
87+
const download = downloadPath(slug, rawPath);
88+
res.type(download.contentType);
89+
res.attachment(download.filename);
90+
if (Buffer.isBuffer(download.body)) {
91+
res.send(download.body);
92+
return;
93+
}
94+
download.body.pipe(res);
95+
}, true));
96+
7597
router.put("/:slug/files/content", apiHandler(async (req, res) => {
7698
const slug = req.params.slug as string;
7799
const authReq = req as AuthenticatedRequest;

src/utils/resource-files.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ interface ResourceFileManagerOptions {
2828
interface ResourceFileManager {
2929
listDirectory: (slug: string, rawPath: string | undefined) => FileTreeResponse;
3030
readFileContent: (slug: string, rawPath: string | undefined) => FileContentResponse;
31+
downloadPath: (slug: string, rawPath: string | undefined) => {
32+
filename: string;
33+
contentType: string;
34+
body: NodeJS.ReadableStream;
35+
};
3136
saveFileContent: (slug: string, request: FileSaveRequest) => FileSaveResponse;
3237
createFileOrFolder: (slug: string, rawParentPath: string | undefined, name: string, kind: "file" | "directory") => FileNode;
3338
uploadFileFromTempPath: (slug: string, rawParentPath: string | undefined, name: string, tempFilePath: string) => FileNode;
@@ -379,6 +384,36 @@ export function createResourceFileManager(options: ResourceFileManagerOptions):
379384
return response;
380385
}
381386

387+
function downloadPath(slug: string, rawPath: string | undefined): {
388+
filename: string;
389+
contentType: string;
390+
body: NodeJS.ReadableStream;
391+
} {
392+
const relativePath = normalizeRelativePath(rawPath ?? "", false);
393+
const absolutePath = resolveTarget(slug, relativePath);
394+
if (!fs.existsSync(absolutePath)) {
395+
throw {
396+
status: 404,
397+
message: "File or folder not found"
398+
};
399+
}
400+
assertExistingPathIsSafe(slug, absolutePath);
401+
402+
const stat = fs.statSync(absolutePath);
403+
if (!stat.isFile()) {
404+
throw {
405+
status: 400,
406+
message: "path must be a file"
407+
};
408+
}
409+
410+
return {
411+
filename: path.basename(relativePath),
412+
contentType: "application/octet-stream",
413+
body: fs.createReadStream(absolutePath),
414+
};
415+
}
416+
382417
function saveFileContent(slug: string, request: FileSaveRequest): FileSaveResponse {
383418
const relativePath = normalizeRelativePath(request.path, false);
384419
const absolutePath = resolveTarget(slug, relativePath);
@@ -560,6 +595,7 @@ export function createResourceFileManager(options: ResourceFileManagerOptions):
560595
return {
561596
listDirectory,
562597
readFileContent,
598+
downloadPath,
563599
saveFileContent,
564600
createFileOrFolder,
565601
uploadFileFromTempPath,

src/utils/skill-files.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const skillFileManager = createResourceFileManager({
1616

1717
export const listSkillDirectory = skillFileManager.listDirectory;
1818
export const readSkillFileContent = skillFileManager.readFileContent;
19+
export const downloadSkillPath = skillFileManager.downloadPath;
1920
export const saveSkillFileContent = skillFileManager.saveFileContent;
2021
export const createSkillFileOrFolder = skillFileManager.createFileOrFolder;
2122
export const uploadSkillFileFromTempPath = skillFileManager.uploadFileFromTempPath;

src/utils/workflow-files.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const workflowFileManager = createResourceFileManager({
99

1010
export const listWorkflowDirectory = workflowFileManager.listDirectory;
1111
export const readWorkflowFileContent = workflowFileManager.readFileContent;
12+
export const downloadWorkflowPath = workflowFileManager.downloadPath;
1213
export const saveWorkflowFileContent = workflowFileManager.saveFileContent;
1314
export const createWorkflowFileOrFolder = workflowFileManager.createFileOrFolder;
1415
export const uploadWorkflowFileFromTempPath = workflowFileManager.uploadFileFromTempPath;

src/workflows/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
} from "../utils/workflow";
1818
import {
1919
createWorkflowFileOrFolder,
20+
downloadWorkflowPath,
2021
deleteWorkflowPath,
2122
listWorkflowDirectory,
2223
readWorkflowFileContent,
@@ -720,6 +721,7 @@ registerResourceFileRoutes({
720721
assertCanEdit: assertCanEditWorkflowFiles,
721722
listDirectory: listWorkflowDirectory,
722723
readFileContent: readWorkflowFileContent,
724+
downloadPath: downloadWorkflowPath,
723725
saveFileContent: saveWorkflowFileContent,
724726
createFileOrFolder: createWorkflowFileOrFolder,
725727
uploadFileFromTempPath: uploadWorkflowFileFromTempPath,

0 commit comments

Comments
 (0)