@fnndsc/chili 3.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/chefs/chefs.d.ts +15 -0
- package/dist/chefs/chefs.js +195 -0
- package/dist/commands/compute/fields.d.ts +6 -0
- package/dist/commands/compute/fields.js +13 -0
- package/dist/commands/compute/list.d.ts +19 -0
- package/dist/commands/compute/list.js +20 -0
- package/dist/commands/connect/login.d.ts +16 -0
- package/dist/commands/connect/login.js +18 -0
- package/dist/commands/connect/logout.d.ts +6 -0
- package/dist/commands/connect/logout.js +16 -0
- package/dist/commands/feed/comments.d.ts +41 -0
- package/dist/commands/feed/comments.js +45 -0
- package/dist/commands/feed/create.d.ts +13 -0
- package/dist/commands/feed/create.js +40 -0
- package/dist/commands/feed/note.d.ts +21 -0
- package/dist/commands/feed/note.js +24 -0
- package/dist/commands/feeds/delete.d.ts +14 -0
- package/dist/commands/feeds/delete.js +34 -0
- package/dist/commands/feeds/fields.d.ts +6 -0
- package/dist/commands/feeds/fields.js +17 -0
- package/dist/commands/feeds/list.d.ts +17 -0
- package/dist/commands/feeds/list.js +38 -0
- package/dist/commands/feeds/share.d.ts +17 -0
- package/dist/commands/feeds/share.js +19 -0
- package/dist/commands/file/view.d.ts +8 -0
- package/dist/commands/file/view.js +25 -0
- package/dist/commands/files/delete.d.ts +16 -0
- package/dist/commands/files/delete.js +36 -0
- package/dist/commands/files/fields.d.ts +7 -0
- package/dist/commands/files/fields.js +18 -0
- package/dist/commands/files/list.d.ts +11 -0
- package/dist/commands/files/list.js +23 -0
- package/dist/commands/fs/cat.d.ts +15 -0
- package/dist/commands/fs/cat.js +26 -0
- package/dist/commands/fs/cp.d.ts +17 -0
- package/dist/commands/fs/cp.js +26 -0
- package/dist/commands/fs/create.d.ts +15 -0
- package/dist/commands/fs/create.js +83 -0
- package/dist/commands/fs/download.d.ts +25 -0
- package/dist/commands/fs/download.js +235 -0
- package/dist/commands/fs/edit.d.ts +16 -0
- package/dist/commands/fs/edit.js +25 -0
- package/dist/commands/fs/ls.d.ts +18 -0
- package/dist/commands/fs/ls.js +142 -0
- package/dist/commands/fs/mkdir.d.ts +8 -0
- package/dist/commands/fs/mkdir.js +21 -0
- package/dist/commands/fs/mv.d.ts +13 -0
- package/dist/commands/fs/mv.js +73 -0
- package/dist/commands/fs/rm.d.ts +24 -0
- package/dist/commands/fs/rm.js +108 -0
- package/dist/commands/fs/touch.d.ts +16 -0
- package/dist/commands/fs/touch.js +62 -0
- package/dist/commands/fs/upload.d.ts +55 -0
- package/dist/commands/fs/upload.js +221 -0
- package/dist/commands/groups/fields.d.ts +6 -0
- package/dist/commands/groups/fields.js +13 -0
- package/dist/commands/groups/list.d.ts +16 -0
- package/dist/commands/groups/list.js +26 -0
- package/dist/commands/man/doc.d.ts +16 -0
- package/dist/commands/man/doc.js +32 -0
- package/dist/commands/man/topics.d.ts +6 -0
- package/dist/commands/man/topics.js +27 -0
- package/dist/commands/pipeline/fields.d.ts +6 -0
- package/dist/commands/pipeline/fields.js +13 -0
- package/dist/commands/pipeline/run.d.ts +18 -0
- package/dist/commands/pipeline/run.js +33 -0
- package/dist/commands/plugin/readme.d.ts +14 -0
- package/dist/commands/plugin/readme.js +49 -0
- package/dist/commands/plugin/run.d.ts +10 -0
- package/dist/commands/plugin/run.js +29 -0
- package/dist/commands/plugin/search.d.ts +7 -0
- package/dist/commands/plugin/search.js +18 -0
- package/dist/commands/plugininstances/fields.d.ts +6 -0
- package/dist/commands/plugininstances/fields.js +13 -0
- package/dist/commands/plugininstances/list.d.ts +16 -0
- package/dist/commands/plugininstances/list.js +26 -0
- package/dist/commands/pluginmetas/fields.d.ts +6 -0
- package/dist/commands/pluginmetas/fields.js +13 -0
- package/dist/commands/pluginmetas/list.d.ts +16 -0
- package/dist/commands/pluginmetas/list.js +26 -0
- package/dist/commands/plugins/add.d.ts +34 -0
- package/dist/commands/plugins/add.js +374 -0
- package/dist/commands/plugins/delete.d.ts +14 -0
- package/dist/commands/plugins/delete.js +34 -0
- package/dist/commands/plugins/fields.d.ts +6 -0
- package/dist/commands/plugins/fields.js +17 -0
- package/dist/commands/plugins/list.d.ts +17 -0
- package/dist/commands/plugins/list.js +40 -0
- package/dist/commands/plugins/overview.d.ts +6 -0
- package/dist/commands/plugins/overview.js +17 -0
- package/dist/commands/store/list.d.ts +23 -0
- package/dist/commands/store/list.js +48 -0
- package/dist/commands/tags/fields.d.ts +6 -0
- package/dist/commands/tags/fields.js +13 -0
- package/dist/commands/tags/list.d.ts +16 -0
- package/dist/commands/tags/list.js +26 -0
- package/dist/commands/workflows/fields.d.ts +6 -0
- package/dist/commands/workflows/fields.js +13 -0
- package/dist/commands/workflows/list.d.ts +16 -0
- package/dist/commands/workflows/list.js +26 -0
- package/dist/config/colorConfig.d.ts +56 -0
- package/dist/config/colorConfig.js +161 -0
- package/dist/connect/connectHandler.d.ts +15 -0
- package/dist/connect/connectHandler.js +48 -0
- package/dist/context/contextCommand.d.ts +13 -0
- package/dist/context/contextCommand.js +193 -0
- package/dist/controllers/baseController.d.ts +43 -0
- package/dist/controllers/baseController.js +67 -0
- package/dist/controllers/feedController.d.ts +35 -0
- package/dist/controllers/feedController.js +53 -0
- package/dist/controllers/fileController.d.ts +56 -0
- package/dist/controllers/fileController.js +93 -0
- package/dist/controllers/pluginContextController.d.ts +24 -0
- package/dist/controllers/pluginContextController.js +49 -0
- package/dist/controllers/pluginController.d.ts +70 -0
- package/dist/controllers/pluginController.js +98 -0
- package/dist/controllers/pluginMetaController.d.ts +41 -0
- package/dist/controllers/pluginMetaController.js +52 -0
- package/dist/feeds/feedHandler.d.ts +64 -0
- package/dist/feeds/feedHandler.js +203 -0
- package/dist/filesystem/fileGroupHandler.d.ts +92 -0
- package/dist/filesystem/fileGroupHandler.js +318 -0
- package/dist/filesystem/filesystemHandler.d.ts +12 -0
- package/dist/filesystem/filesystemHandler.js +135 -0
- package/dist/filesystem/inodeCommand.d.ts +12 -0
- package/dist/filesystem/inodeCommand.js +37 -0
- package/dist/handlers/baseGroupHandler.d.ts +103 -0
- package/dist/handlers/baseGroupHandler.js +342 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +239 -0
- package/dist/lfs/lfs.d.ts +25 -0
- package/dist/lfs/lfs.js +74 -0
- package/dist/man/man.d.ts +15 -0
- package/dist/man/man.js +31 -0
- package/dist/man/renderer.d.ts +26 -0
- package/dist/man/renderer.js +187 -0
- package/dist/models/feed.d.ts +18 -0
- package/dist/models/feed.js +8 -0
- package/dist/models/listing.d.ts +33 -0
- package/dist/models/listing.js +9 -0
- package/dist/models/plugin.d.ts +27 -0
- package/dist/models/plugin.js +1 -0
- package/dist/models/resource.d.ts +39 -0
- package/dist/models/resource.js +9 -0
- package/dist/pacs/pacsQueryHandler.d.ts +55 -0
- package/dist/pacs/pacsQueryHandler.js +196 -0
- package/dist/pacs/pacsQueryPayload.d.ts +19 -0
- package/dist/pacs/pacsQueryPayload.js +44 -0
- package/dist/pacs/pacsResultRender.d.ts +20 -0
- package/dist/pacs/pacsResultRender.js +141 -0
- package/dist/pacs/pacsRetrieveHandler.d.ts +87 -0
- package/dist/pacs/pacsRetrieveHandler.js +272 -0
- package/dist/pacs/pacsServerHandler.d.ts +15 -0
- package/dist/pacs/pacsServerHandler.js +32 -0
- package/dist/path/pathCommand.d.ts +64 -0
- package/dist/path/pathCommand.js +805 -0
- package/dist/path/pathMapper.d.ts +189 -0
- package/dist/path/pathMapper.js +382 -0
- package/dist/plugins/pluginGroupHandler.d.ts +42 -0
- package/dist/plugins/pluginGroupHandler.js +90 -0
- package/dist/plugins/pluginHandler.d.ts +87 -0
- package/dist/plugins/pluginHandler.js +303 -0
- package/dist/plugins/pluginMetaHandler.d.ts +21 -0
- package/dist/plugins/pluginMetaHandler.js +27 -0
- package/dist/screen/screen.d.ts +214 -0
- package/dist/screen/screen.js +478 -0
- package/dist/utils/admin_prompt.d.ts +42 -0
- package/dist/utils/admin_prompt.js +105 -0
- package/dist/utils/cli.d.ts +67 -0
- package/dist/utils/cli.js +112 -0
- package/dist/utils/docker.d.ts +74 -0
- package/dist/utils/docker.js +155 -0
- package/dist/utils/input_format.d.ts +77 -0
- package/dist/utils/input_format.js +135 -0
- package/dist/utils/sort.d.ts +29 -0
- package/dist/utils/sort.js +63 -0
- package/dist/utils/ui.d.ts +7 -0
- package/dist/utils/ui.js +26 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.js +14 -0
- package/dist/views/compute.d.ts +17 -0
- package/dist/views/compute.js +40 -0
- package/dist/views/connect.d.ts +13 -0
- package/dist/views/connect.js +35 -0
- package/dist/views/feed.d.ts +44 -0
- package/dist/views/feed.js +110 -0
- package/dist/views/file.d.ts +25 -0
- package/dist/views/file.js +71 -0
- package/dist/views/fs.d.ts +51 -0
- package/dist/views/fs.js +105 -0
- package/dist/views/ls.d.ts +37 -0
- package/dist/views/ls.js +190 -0
- package/dist/views/plugin.d.ts +28 -0
- package/dist/views/plugin.js +71 -0
- package/dist/views/pluginParameters.d.ts +17 -0
- package/dist/views/pluginParameters.js +79 -0
- package/docs/00_intro.adoc +25 -0
- package/docs/01_setup.adoc +63 -0
- package/docs/02_context.adoc +128 -0
- package/docs/03_searchable.adoc +134 -0
- package/docs/10_everyday_examples.adoc +9 -0
- package/docs/11_upload_data.adoc +146 -0
- package/docs/12_create_a_feed.adoc +9 -0
- package/docs/13_run_a_plugin.adoc +271 -0
- package/docs/20_plugins.adoc +16 -0
- package/docs/21_pluginReadme.adoc +123 -0
- package/docs/30_filesystem.adoc +93 -0
- package/docs/31_fileops.adoc +51 -0
- package/docs/32_file_content.adoc +471 -0
- package/docs/_architecture.adoc +48 -0
- package/docs/_deepContext.adoc +84 -0
- package/docs/_roadmap.adoc +42 -0
- package/docs/_searchable.adoc +187 -0
- package/docs/_state.adoc +68 -0
- package/docs/pacs.adoc +101 -0
- package/docs/pathMapper.md +246 -0
- package/package.json +92 -0
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Path scan/transfer commands and the recursive ChRIS path scanner.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { chrisIO, chrisContext, Context, errorStack, objContext_create, } from "@fnndsc/cumin";
|
|
7
|
+
import { vfsDispatcher } from "@fnndsc/salsa";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import cliProgress from "cli-progress";
|
|
12
|
+
import { table_display, border_draw } from "../screen/screen.js";
|
|
13
|
+
import { options_toParams } from "../utils/cli.js";
|
|
14
|
+
import archy from "archy";
|
|
15
|
+
import open from "open";
|
|
16
|
+
import { exec } from "child_process";
|
|
17
|
+
import os from "os";
|
|
18
|
+
import { files_downloadWithProgress } from "../commands/fs/download.js";
|
|
19
|
+
/**
|
|
20
|
+
* Creates a summary table for file transfer details.
|
|
21
|
+
*
|
|
22
|
+
* @param summary - The TransferDetail object containing transfer statistics.
|
|
23
|
+
* @returns A 2D array representing the summary table.
|
|
24
|
+
*/
|
|
25
|
+
function summaryTable_create(summary) {
|
|
26
|
+
const summaryTable = [
|
|
27
|
+
["Total files", summary.totalFiles.toString()],
|
|
28
|
+
[
|
|
29
|
+
"Successfully transferred",
|
|
30
|
+
chalk.green(summary.transferredCount.toString()),
|
|
31
|
+
],
|
|
32
|
+
["Failed to transfer", chalk.red(summary.failedCount.toString())],
|
|
33
|
+
[
|
|
34
|
+
"Total data transferred",
|
|
35
|
+
chalk.blueBright(bytes_format(summary.transferSize)),
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
"Average transfer speed",
|
|
39
|
+
chalk.blueBright(`${bytes_format(summary.speed)}/s`),
|
|
40
|
+
],
|
|
41
|
+
["Duration", `${summary.duration.toFixed(2)} seconds`],
|
|
42
|
+
];
|
|
43
|
+
return summaryTable;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates resource groups (files, directories, links) for a given ChRIS path.
|
|
47
|
+
*
|
|
48
|
+
* @param currentPath - The ChRIS path to create resource groups for.
|
|
49
|
+
* @returns A Promise resolving to an object containing the resource groups, or null on error.
|
|
50
|
+
*/
|
|
51
|
+
async function resourceGroups_create(currentPath) {
|
|
52
|
+
try {
|
|
53
|
+
const filesGroup = (await objContext_create("ChRISFilesContext", `folder:${currentPath}`));
|
|
54
|
+
const dirsGroup = (await objContext_create("ChRISDirsContext", `folder:${currentPath}`));
|
|
55
|
+
const linksGroup = (await objContext_create("ChRISLinksContext", `folder:${currentPath}`));
|
|
56
|
+
return { filesGroup, dirsGroup, linksGroup };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
errorStack.stack_push("error", `Failed to create ChRISEmbeddedResourceGroup objects for path ${currentPath}: ${error}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Generates a Mermaid diagram definition from a scan result.
|
|
65
|
+
*
|
|
66
|
+
* @param scanResult - The ScanRecord containing file information.
|
|
67
|
+
* @returns The Mermaid diagram definition string.
|
|
68
|
+
*/
|
|
69
|
+
function mermaidDefinition_generate(scanResult) {
|
|
70
|
+
let definition = "graph TD\n";
|
|
71
|
+
definition += " %% Styles\n";
|
|
72
|
+
definition +=
|
|
73
|
+
" classDef default fill:#f9f,stroke:#333,stroke-width:2px;\n";
|
|
74
|
+
definition += " classDef root fill:#ff9,stroke:#333,stroke-width:4px;\n";
|
|
75
|
+
definition += " classDef leaf fill:#9f9,stroke:#333,stroke-width:2px;\n\n";
|
|
76
|
+
const nodes = new Map();
|
|
77
|
+
const edges = new Set();
|
|
78
|
+
let nodeCounter = 0;
|
|
79
|
+
function nodeId_getOrCreate(path) {
|
|
80
|
+
if (!nodes.has(path)) {
|
|
81
|
+
nodes.set(path, nodeCounter++);
|
|
82
|
+
}
|
|
83
|
+
return nodes.get(path);
|
|
84
|
+
}
|
|
85
|
+
const leafNodes = new Set();
|
|
86
|
+
scanResult.fileInfo.forEach((file) => {
|
|
87
|
+
const parts = file.chrisPath.split("/").filter(Boolean);
|
|
88
|
+
for (let i = 1; i < parts.length; i++) {
|
|
89
|
+
const parentPath = "/" + parts.slice(0, i).join("/");
|
|
90
|
+
const currentPath = "/" + parts.slice(0, i + 1).join("/");
|
|
91
|
+
const parentNode = nodeId_getOrCreate(parentPath);
|
|
92
|
+
const currentNode = nodeId_getOrCreate(currentPath);
|
|
93
|
+
const edge = `${parentNode} --> ${currentNode}`;
|
|
94
|
+
if (!edges.has(edge)) {
|
|
95
|
+
edges.add(edge);
|
|
96
|
+
definition += ` ${parentNode}[${parts[i - 1]}] --> ${currentNode}[${parts[i]}]
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
if (i === parts.length - 1) {
|
|
100
|
+
leafNodes.add(currentNode);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// Apply styles
|
|
105
|
+
definition += "\n %% Applying styles\n";
|
|
106
|
+
definition += ` class ${nodeId_getOrCreate("/")} root;\n`;
|
|
107
|
+
leafNodes.forEach((nodeId) => {
|
|
108
|
+
definition += ` class ${nodeId} leaf;\n`;
|
|
109
|
+
});
|
|
110
|
+
return definition;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generates an HTML page for displaying a Mermaid diagram.
|
|
114
|
+
*
|
|
115
|
+
* @param mermaidDefinition - The Mermaid diagram definition string.
|
|
116
|
+
* @returns The HTML content as a string.
|
|
117
|
+
*/
|
|
118
|
+
function mermaidHtml_generate(mermaidDefinition) {
|
|
119
|
+
return `
|
|
120
|
+
<!DOCTYPE html>
|
|
121
|
+
<html lang="en">
|
|
122
|
+
<head>
|
|
123
|
+
<meta charset="UTF-8">
|
|
124
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
125
|
+
<title>Plugin DAG</title>
|
|
126
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div class="mermaid">
|
|
130
|
+
${mermaidDefinition}
|
|
131
|
+
</div>
|
|
132
|
+
<script>
|
|
133
|
+
mermaid.initialize({ startOnLoad: true });
|
|
134
|
+
</script>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
137
|
+
`;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Renders a Mermaid diagram in a browser window.
|
|
141
|
+
*
|
|
142
|
+
* @param mermaidDefinition - The Mermaid diagram definition string.
|
|
143
|
+
*/
|
|
144
|
+
async function mermaid_renderInBrowser(mermaidDefinition) {
|
|
145
|
+
const html = mermaidHtml_generate(mermaidDefinition);
|
|
146
|
+
const tempDir = os.tmpdir();
|
|
147
|
+
const filePath = path.join(tempDir, "mermaid-diagram.html");
|
|
148
|
+
fs.writeFileSync(filePath, html);
|
|
149
|
+
await open(filePath);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Renders a Mermaid diagram on the server-side and saves it to a file.
|
|
153
|
+
* Requires `mmdc` (Mermaid CLI) to be installed.
|
|
154
|
+
*
|
|
155
|
+
* @param mermaidDefinition - The Mermaid diagram definition string.
|
|
156
|
+
* @param outputFile - The path to save the output file.
|
|
157
|
+
* @returns A Promise resolving to the output file path.
|
|
158
|
+
*/
|
|
159
|
+
async function mermaid_renderServerSide(mermaidDefinition, outputFile) {
|
|
160
|
+
const tempDir = os.tmpdir();
|
|
161
|
+
const inputFile = path.join(tempDir, "input.mmd");
|
|
162
|
+
fs.writeFileSync(inputFile, mermaidDefinition);
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
exec(`npx mmdc -i ${inputFile} -o ${outputFile}`, (error, stdout, stderr) => {
|
|
165
|
+
if (error) {
|
|
166
|
+
console.error(`exec error: ${error}`);
|
|
167
|
+
reject(error);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolve(outputFile);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Creates an Archy-formatted tree structure from file information.
|
|
176
|
+
*
|
|
177
|
+
* @param files - An array of FileInfo objects.
|
|
178
|
+
* @returns The Archy-formatted tree string.
|
|
179
|
+
*/
|
|
180
|
+
export function archyTree_create(files) {
|
|
181
|
+
const root = { label: "", nodes: {} };
|
|
182
|
+
files.forEach((file) => {
|
|
183
|
+
if (file.chrisPath) {
|
|
184
|
+
const parts = file.chrisPath.split("/").filter(Boolean);
|
|
185
|
+
let current = root;
|
|
186
|
+
parts.forEach((part, index) => {
|
|
187
|
+
if (!current.nodes[part]) {
|
|
188
|
+
current.nodes[part] = { label: part, nodes: {} };
|
|
189
|
+
}
|
|
190
|
+
current = current.nodes[part];
|
|
191
|
+
if (index === parts.length - 1 && file.isLink) {
|
|
192
|
+
current.label += ` -> ${file.linkTarget}`;
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
function archyFormat_convert(node) {
|
|
198
|
+
return {
|
|
199
|
+
label: node.label,
|
|
200
|
+
nodes: Object.values(node.nodes).map(archyFormat_convert),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return archy(archyFormat_convert(root));
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Scans directories within ChRIS.
|
|
207
|
+
*
|
|
208
|
+
* @param dirsGroup - The ChRISEmbeddedResourceGroup for directories.
|
|
209
|
+
* @param chrisPath - The base ChRIS path.
|
|
210
|
+
* @param hostBasePath - The base path on the host system.
|
|
211
|
+
* @param linkedPath - Optional linked path for resolving relative paths.
|
|
212
|
+
* @returns A Promise resolving to an array of FileInfo objects for directories.
|
|
213
|
+
*/
|
|
214
|
+
async function dirs_scan(dirsGroup, chrisPath, hostBasePath, linkedPath = "") {
|
|
215
|
+
const dirs = [];
|
|
216
|
+
const dirResults = await dirsGroup.asset.resources_listAndFilterByOptions(options_toParams({ fields: "path" }));
|
|
217
|
+
if (dirResults && dirResults.tableData) {
|
|
218
|
+
for (const dir of dirResults.tableData) {
|
|
219
|
+
const dirPath = "/" + String(dir.path);
|
|
220
|
+
const relativeChrisPath = linkedPath
|
|
221
|
+
? path.join(linkedPath, path.basename(dirPath))
|
|
222
|
+
: dirPath;
|
|
223
|
+
const dirInfo = {
|
|
224
|
+
id: 0,
|
|
225
|
+
hostPath: path.join(hostBasePath, path.relative(chrisPath, relativeChrisPath)),
|
|
226
|
+
chrisPath: relativeChrisPath,
|
|
227
|
+
size: 0,
|
|
228
|
+
isLink: false,
|
|
229
|
+
linkTarget: "",
|
|
230
|
+
isDirectory: true,
|
|
231
|
+
};
|
|
232
|
+
dirs.push(dirInfo);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return dirs;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Scans files within ChRIS.
|
|
239
|
+
*
|
|
240
|
+
* @param filesGroup - The ChRISEmbeddedResourceGroup for files.
|
|
241
|
+
* @param chrisPath - The base ChRIS path.
|
|
242
|
+
* @param hostBasePath - The base path on the host system.
|
|
243
|
+
* @param linkedPath - Optional linked path for resolving relative paths.
|
|
244
|
+
* @returns A Promise resolving to an array of FileInfo objects for files.
|
|
245
|
+
*/
|
|
246
|
+
async function files_scan(filesGroup, chrisPath, hostBasePath, linkedPath = "") {
|
|
247
|
+
const files = [];
|
|
248
|
+
const fileResults = await filesGroup.asset.resources_listAndFilterByOptions(options_toParams({ fields: "id,fname,fsize" }));
|
|
249
|
+
if (fileResults && fileResults.tableData) {
|
|
250
|
+
for (const file of fileResults.tableData) {
|
|
251
|
+
const size = parseInt(String(file.fsize), 10);
|
|
252
|
+
const fname = "/" + String(file.fname);
|
|
253
|
+
const relativeChrisPath = linkedPath
|
|
254
|
+
? path.join(linkedPath, path.basename(fname))
|
|
255
|
+
: fname;
|
|
256
|
+
const fileInfo = {
|
|
257
|
+
id: parseInt(String(file.id), 10),
|
|
258
|
+
hostPath: path.join(hostBasePath, path.relative(chrisPath, relativeChrisPath)),
|
|
259
|
+
chrisPath: relativeChrisPath,
|
|
260
|
+
size: isNaN(size) ? 0 : size,
|
|
261
|
+
isLink: !!linkedPath,
|
|
262
|
+
linkTarget: linkedPath ? fname : "",
|
|
263
|
+
isDirectory: false,
|
|
264
|
+
};
|
|
265
|
+
files.push(fileInfo);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return files;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Scans links within ChRIS.
|
|
272
|
+
*
|
|
273
|
+
* @param linksGroup - The ChRISEmbeddedResourceGroup for links.
|
|
274
|
+
* @param chrisPath - The base ChRIS path.
|
|
275
|
+
* @param hostBasePath - The base path on the host system.
|
|
276
|
+
* @param linkedPath - Optional linked path for resolving relative paths.
|
|
277
|
+
* @returns A Promise resolving to an array of FileInfo objects for links.
|
|
278
|
+
*/
|
|
279
|
+
async function links_scan(linksGroup, chrisPath, hostBasePath, linkedPath = "") {
|
|
280
|
+
const links = [];
|
|
281
|
+
const linkResults = await linksGroup.asset.resources_listAndFilterByOptions(options_toParams({ fields: "id,path,fname" }));
|
|
282
|
+
if (linkResults && linkResults.tableData) {
|
|
283
|
+
for (const link of linkResults.tableData) {
|
|
284
|
+
const linkPath = "/" + String(link.path);
|
|
285
|
+
const linkFname = "/" + String(link.fname);
|
|
286
|
+
const relativeChrisPath = linkedPath
|
|
287
|
+
? path.join(linkedPath, path.basename(linkFname))
|
|
288
|
+
: linkFname;
|
|
289
|
+
const linkInfo = {
|
|
290
|
+
id: parseInt(String(link.id), 10),
|
|
291
|
+
hostPath: path.join(hostBasePath, path.relative(chrisPath, relativeChrisPath)),
|
|
292
|
+
chrisPath: relativeChrisPath,
|
|
293
|
+
isLink: true,
|
|
294
|
+
linkTarget: linkPath,
|
|
295
|
+
size: 0,
|
|
296
|
+
isDirectory: false,
|
|
297
|
+
};
|
|
298
|
+
links.push(linkInfo);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return links;
|
|
302
|
+
}
|
|
303
|
+
async function chrisDir_walk(currentPath, chrisPath, hostBasePath, followLinks, dirsOnly, linkedPath = "") {
|
|
304
|
+
const listResult = await vfsDispatcher.list(currentPath);
|
|
305
|
+
if (!listResult.ok) {
|
|
306
|
+
return { files: [], totalSize: 0 };
|
|
307
|
+
}
|
|
308
|
+
const items = listResult.value || [];
|
|
309
|
+
const dirs = items.filter((item) => item.type === "dir" || item.type === "vfs" || item.type === "job");
|
|
310
|
+
const normalFiles = items.filter((item) => item.type === "file");
|
|
311
|
+
const links = items.filter((item) => item.type === "link");
|
|
312
|
+
const files = [];
|
|
313
|
+
let totalSize = 0;
|
|
314
|
+
const dirInfos = dirs.map((dir) => {
|
|
315
|
+
const fullPath = currentPath === "/" ? "/" + dir.name : path.join(currentPath, dir.name);
|
|
316
|
+
const relativeChrisPath = linkedPath ? path.join(linkedPath, dir.name) : fullPath;
|
|
317
|
+
return {
|
|
318
|
+
id: 0,
|
|
319
|
+
hostPath: path.join(hostBasePath, path.relative(chrisPath, relativeChrisPath)),
|
|
320
|
+
chrisPath: relativeChrisPath,
|
|
321
|
+
size: 0,
|
|
322
|
+
isLink: false,
|
|
323
|
+
linkTarget: "",
|
|
324
|
+
isDirectory: true,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
files.push(...dirInfos);
|
|
328
|
+
if (!dirsOnly) {
|
|
329
|
+
const fileInfos = normalFiles.map((file) => {
|
|
330
|
+
const fullPath = currentPath === "/" ? "/" + file.name : path.join(currentPath, file.name);
|
|
331
|
+
const relativeChrisPath = linkedPath ? path.join(linkedPath, file.name) : fullPath;
|
|
332
|
+
return {
|
|
333
|
+
id: 0,
|
|
334
|
+
hostPath: path.join(hostBasePath, path.relative(chrisPath, relativeChrisPath)),
|
|
335
|
+
chrisPath: relativeChrisPath,
|
|
336
|
+
size: file.size,
|
|
337
|
+
isLink: !!linkedPath,
|
|
338
|
+
linkTarget: linkedPath ? fullPath : "",
|
|
339
|
+
isDirectory: false,
|
|
340
|
+
};
|
|
341
|
+
});
|
|
342
|
+
files.push(...fileInfos);
|
|
343
|
+
totalSize += fileInfos.reduce((sum, f) => sum + f.size, 0);
|
|
344
|
+
const linkInfos = links.map((link) => {
|
|
345
|
+
const fullPath = currentPath === "/" ? "/" + link.name : path.join(currentPath, link.name);
|
|
346
|
+
const relativeChrisPath = linkedPath ? path.join(linkedPath, link.name) : fullPath;
|
|
347
|
+
return {
|
|
348
|
+
id: 0,
|
|
349
|
+
hostPath: path.join(hostBasePath, path.relative(chrisPath, relativeChrisPath)),
|
|
350
|
+
chrisPath: relativeChrisPath,
|
|
351
|
+
isLink: true,
|
|
352
|
+
linkTarget: link.target || "",
|
|
353
|
+
size: 0,
|
|
354
|
+
isDirectory: false,
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
files.push(...linkInfos);
|
|
358
|
+
if (followLinks) {
|
|
359
|
+
for (const link of linkInfos) {
|
|
360
|
+
const sub = await chrisDir_walk(link.linkTarget, chrisPath, hostBasePath, followLinks, dirsOnly, link.chrisPath);
|
|
361
|
+
files.push(...sub.files);
|
|
362
|
+
totalSize += sub.totalSize;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const dir of dirs) {
|
|
367
|
+
const fullPath = currentPath === "/" ? "/" + dir.name : path.join(currentPath, dir.name);
|
|
368
|
+
const nextLinkedPath = linkedPath ? path.join(linkedPath, dir.name) : "";
|
|
369
|
+
const sub = await chrisDir_walk(fullPath, chrisPath, hostBasePath, followLinks, dirsOnly, nextLinkedPath);
|
|
370
|
+
files.push(...sub.files);
|
|
371
|
+
totalSize += sub.totalSize;
|
|
372
|
+
}
|
|
373
|
+
return { files, totalSize };
|
|
374
|
+
}
|
|
375
|
+
async function chrisFS_scan(chrisPath, hostBasePath, followLinks = false, dirsOnly = false) {
|
|
376
|
+
try {
|
|
377
|
+
const result = await chrisDir_walk(chrisPath, chrisPath, hostBasePath, followLinks, dirsOnly);
|
|
378
|
+
return { fileInfo: result.files, totalSize: result.totalSize };
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
errorStack.stack_push("error", `Failed to scan ChRIS filesystem: ${error}`);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function scanResult_filter(scanResult, options) {
|
|
386
|
+
if (!options.filter && !options.endsWith) {
|
|
387
|
+
return scanResult;
|
|
388
|
+
}
|
|
389
|
+
const keepPaths = new Set();
|
|
390
|
+
for (const file of scanResult.fileInfo) {
|
|
391
|
+
const basename = path.basename(file.chrisPath);
|
|
392
|
+
if ((!options.filter || file.chrisPath.includes(options.filter)) &&
|
|
393
|
+
(!options.endsWith || basename.includes(options.endsWith))) {
|
|
394
|
+
let currentPath = file.chrisPath;
|
|
395
|
+
while (currentPath !== "/") {
|
|
396
|
+
keepPaths.add(currentPath);
|
|
397
|
+
currentPath = path.dirname(currentPath);
|
|
398
|
+
}
|
|
399
|
+
keepPaths.add("/");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const fileInfo = scanResult.fileInfo.filter((f) => keepPaths.has(f.chrisPath));
|
|
403
|
+
return { fileInfo, totalSize: fileInfo.reduce((sum, f) => sum + f.size, 0) };
|
|
404
|
+
}
|
|
405
|
+
async function scanResult_render(scanResult, options) {
|
|
406
|
+
if (options.mermaid) {
|
|
407
|
+
const mermaidDefinition = mermaidDefinition_generate(scanResult);
|
|
408
|
+
if (options.save) {
|
|
409
|
+
try {
|
|
410
|
+
const outputFile = path.resolve(options.save);
|
|
411
|
+
const savedFilePath = await mermaid_renderServerSide(mermaidDefinition, outputFile);
|
|
412
|
+
console.log(`Mermaid diagram saved to: ${savedFilePath}`);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
console.error(`Failed to save Mermaid diagram: ${error}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
await mermaid_renderInBrowser(mermaidDefinition);
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (options.tree) {
|
|
424
|
+
console.log(archyTree_create(scanResult.fileInfo));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!options.silent) {
|
|
428
|
+
for (const file of scanResult.fileInfo) {
|
|
429
|
+
if (file.isLink && !options.follow) {
|
|
430
|
+
console.log(`${file.chrisPath} -> ${file.linkTarget}`);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
console.log(`${file.chrisPath}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Performs a recursive scan of the current ChRIS folder.
|
|
440
|
+
*
|
|
441
|
+
* @param options - Scan options.
|
|
442
|
+
* @returns The scan record, or null if no context is set.
|
|
443
|
+
*/
|
|
444
|
+
export async function scan_do(options) {
|
|
445
|
+
const chrisFolder = await chrisContext.current_get(Context.ChRISfolder);
|
|
446
|
+
if (!chrisFolder) {
|
|
447
|
+
console.error(chalk.red("No ChRIS folder context set. Use 'folder=' to set a context."));
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
if (!options.silent) {
|
|
451
|
+
console.log(chalk.cyan(`Scanning for ${options.dirsOnly ? "directories" : "all files"} recursively from ${chrisFolder}`));
|
|
452
|
+
}
|
|
453
|
+
const hostBasePath = options.hostpath || process.cwd();
|
|
454
|
+
const scanResult = await chrisFS_scan(chrisFolder, hostBasePath, options.follow, options.dirsOnly);
|
|
455
|
+
if (!scanResult) {
|
|
456
|
+
console.error(chalk.red("Failed to scan ChRIS filesystem."));
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const filtered = scanResult_filter(scanResult, options);
|
|
460
|
+
await scanResult_render(filtered, options);
|
|
461
|
+
if (!options.silent) {
|
|
462
|
+
console.log(chalk.green(`Total size: ${bytes_format(filtered.totalSize)}`));
|
|
463
|
+
if (options.filter || options.endsWith) {
|
|
464
|
+
console.log(chalk.cyan(`Filtered results: ${filtered.fileInfo.length} items`));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return filtered;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Recursively gets files from the host path to be uploaded to ChRIS.
|
|
471
|
+
*
|
|
472
|
+
* @param hostpath - The host system path to scan.
|
|
473
|
+
* @param chrisdir - The target ChRIS directory path.
|
|
474
|
+
* @returns A Promise resolving to an array of FileInfo objects for files to upload.
|
|
475
|
+
*/
|
|
476
|
+
async function filesToUpload_get(hostpath, chrisdir) {
|
|
477
|
+
const files = [];
|
|
478
|
+
let idCounter = 1;
|
|
479
|
+
async function dir_walk(currentPath, currentChrisPath) {
|
|
480
|
+
const entries = await fs.promises.readdir(currentPath, {
|
|
481
|
+
withFileTypes: true,
|
|
482
|
+
});
|
|
483
|
+
for (const entry of entries) {
|
|
484
|
+
const hostFilePath = path.join(currentPath, entry.name);
|
|
485
|
+
const chrisFilePath = path.join(currentChrisPath, entry.name);
|
|
486
|
+
if (entry.isDirectory()) {
|
|
487
|
+
await dir_walk(hostFilePath, chrisFilePath);
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
files.push({
|
|
491
|
+
id: idCounter++,
|
|
492
|
+
hostPath: hostFilePath,
|
|
493
|
+
chrisPath: chrisFilePath,
|
|
494
|
+
isLink: false, // Assuming local files are not symlinks
|
|
495
|
+
size: (await fs.promises.stat(hostFilePath)).size,
|
|
496
|
+
linkTarget: "",
|
|
497
|
+
isDirectory: false,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Preserve the basename of the source directory in the target path
|
|
503
|
+
const hostBasename = path.basename(hostpath);
|
|
504
|
+
await dir_walk(hostpath, path.join(chrisdir, hostBasename));
|
|
505
|
+
return files;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Scans the local filesystem for files to upload to ChRIS.
|
|
509
|
+
*
|
|
510
|
+
* @param options - TransferCLI options specifying the host path.
|
|
511
|
+
* @returns A Promise resolving to a ScanRecord of local files, or null on error.
|
|
512
|
+
*/
|
|
513
|
+
async function localFS_scan(options) {
|
|
514
|
+
await chrisContext.currentContext_update();
|
|
515
|
+
const folder = chrisContext.singleContext.folder;
|
|
516
|
+
if (!folder) {
|
|
517
|
+
console.error("ChRIS folder context is undefined, cannot initialize chrisIO.");
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
// Validate that the folder context exists in CUBE before proceeding
|
|
521
|
+
try {
|
|
522
|
+
const testGroup = await objContext_create("ChRISDirsContext", `folder:${folder}`);
|
|
523
|
+
if (!testGroup) {
|
|
524
|
+
console.error(chalk.red(`Folder context '${folder}' does not exist in CUBE. Please specify an existing directory.`));
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
console.error(chalk.red(`Folder context '${folder}' does not exist in CUBE. Please specify an existing directory.`));
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
// Folder exists, so just set chrisFolder without calling init()
|
|
533
|
+
// (init() tries to CREATE the folder which will fail if it already exists)
|
|
534
|
+
chrisIO.chrisFolder = folder;
|
|
535
|
+
console.log(border_draw(chalk.cyan("Scanning files to upload...")));
|
|
536
|
+
const filesToUpload = await filesToUpload_get(options.hostpath, folder);
|
|
537
|
+
let totalSize = 0;
|
|
538
|
+
for (const file of filesToUpload) {
|
|
539
|
+
if (file.hostPath) {
|
|
540
|
+
const stats = await fs.promises.stat(file.hostPath);
|
|
541
|
+
totalSize += stats.size;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
fileInfo: filesToUpload,
|
|
546
|
+
totalSize: totalSize,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Sets up and starts a CLI progress bar for file transfers.
|
|
551
|
+
*
|
|
552
|
+
* @param scanRecord - The ScanRecord containing total files and size.
|
|
553
|
+
* @returns The initialized cliProgress.SingleBar instance.
|
|
554
|
+
*/
|
|
555
|
+
function progressBar_setupAndStart(scanRecord) {
|
|
556
|
+
const progressBar = new cliProgress.SingleBar({
|
|
557
|
+
format: "Transferring [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} files | {bytes}/{totalBytes}",
|
|
558
|
+
}, cliProgress.Presets.shades_classic);
|
|
559
|
+
progressBar.start(scanRecord.fileInfo.length, 0, {
|
|
560
|
+
bytes: "0 B",
|
|
561
|
+
totalBytes: bytes_format(scanRecord.totalSize), // Renamed
|
|
562
|
+
});
|
|
563
|
+
return progressBar;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Initializes a TransferDetail object.
|
|
567
|
+
*
|
|
568
|
+
* @returns An initialized TransferDetail object.
|
|
569
|
+
*/
|
|
570
|
+
function transferDetail_init() {
|
|
571
|
+
const uploadSummary = {
|
|
572
|
+
startTime: 0,
|
|
573
|
+
endTime: 0,
|
|
574
|
+
totalFiles: 0,
|
|
575
|
+
transferredCount: 0,
|
|
576
|
+
failedCount: 0,
|
|
577
|
+
transferSize: 0,
|
|
578
|
+
speed: 0,
|
|
579
|
+
duration: 0,
|
|
580
|
+
};
|
|
581
|
+
return uploadSummary;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Calculates transmission summary details (duration and speed).
|
|
585
|
+
*
|
|
586
|
+
* @param transfer - The TransferDetail object to update.
|
|
587
|
+
* @returns A Tranmission object with calculated speed and duration.
|
|
588
|
+
*/
|
|
589
|
+
function transmissionSummary_get(transfer) {
|
|
590
|
+
let transmission = {
|
|
591
|
+
duration: 0,
|
|
592
|
+
speed: 0,
|
|
593
|
+
};
|
|
594
|
+
transfer.endTime = Date.now();
|
|
595
|
+
transmission.duration = (transfer.endTime - transfer.startTime) / 1000; // in seconds
|
|
596
|
+
transmission.speed = transfer.transferSize / transmission.duration; // bytes per second
|
|
597
|
+
return transmission;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Resolves the parent directory name for pulled files.
|
|
601
|
+
*
|
|
602
|
+
* @returns The parent directory name or an empty string.
|
|
603
|
+
*/
|
|
604
|
+
function pulledParentDir_resolve() {
|
|
605
|
+
const chrisFolder = chrisContext.singleContext.folder;
|
|
606
|
+
if (!chrisFolder) {
|
|
607
|
+
return "";
|
|
608
|
+
}
|
|
609
|
+
if (chrisFolder.endsWith("/")) {
|
|
610
|
+
return "";
|
|
611
|
+
}
|
|
612
|
+
return path.basename(chrisFolder);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Pulls files from ChRIS to the local filesystem.
|
|
616
|
+
*
|
|
617
|
+
* @param scanRecord - The ScanRecord of files to pull.
|
|
618
|
+
* @param progressBar - The CLI progress bar instance.
|
|
619
|
+
* @returns A Promise resolving to a TransferDetail object with pull statistics.
|
|
620
|
+
*/
|
|
621
|
+
async function chris_pull(scanRecord, progressBar) {
|
|
622
|
+
let summary = transferDetail_init();
|
|
623
|
+
summary.startTime = Date.now();
|
|
624
|
+
const parentDir = pulledParentDir_resolve();
|
|
625
|
+
const baseDir = process.cwd();
|
|
626
|
+
for (const [index, file] of scanRecord.fileInfo.entries()) {
|
|
627
|
+
try {
|
|
628
|
+
if (!file.hostPath) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const fileBuffer = await chrisIO.file_download(file.id);
|
|
632
|
+
if (fileBuffer) {
|
|
633
|
+
const relativePath = path.relative(baseDir, file.hostPath);
|
|
634
|
+
const hostPath = path.join(baseDir, parentDir, relativePath);
|
|
635
|
+
const dirPath = path.dirname(hostPath);
|
|
636
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
637
|
+
fs.writeFileSync(hostPath, fileBuffer);
|
|
638
|
+
summary.transferredCount++;
|
|
639
|
+
summary.transferSize += fileBuffer.length;
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
summary.failedCount++;
|
|
643
|
+
console.log(chalk.yellow(`Failed to download: ${file.hostPath}`));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
summary.failedCount++;
|
|
648
|
+
console.log(chalk.red(`Error downloading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
|
|
649
|
+
}
|
|
650
|
+
progressBar.update(index + 1, {
|
|
651
|
+
bytes: bytes_format(summary.transferSize), // Renamed
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const transmission = transmissionSummary_get(summary);
|
|
655
|
+
progressBar.stop();
|
|
656
|
+
summary.totalFiles = scanRecord.fileInfo.length;
|
|
657
|
+
summary.duration = transmission.duration;
|
|
658
|
+
summary.speed = transmission.speed;
|
|
659
|
+
return summary;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Pushes files from the local filesystem to ChRIS.
|
|
663
|
+
*
|
|
664
|
+
* @param scanRecord - The ScanRecord of files to push.
|
|
665
|
+
* @param progressBar - The CLI progress bar instance.
|
|
666
|
+
* @returns A Promise resolving to a TransferDetail object with push statistics.
|
|
667
|
+
*/
|
|
668
|
+
async function chris_push(scanRecord, progressBar) {
|
|
669
|
+
let summary = transferDetail_init();
|
|
670
|
+
summary.startTime = Date.now();
|
|
671
|
+
for (const [index, file] of scanRecord.fileInfo.entries()) {
|
|
672
|
+
try {
|
|
673
|
+
if (!file.hostPath) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const fileContent = await fs.promises.readFile(file.hostPath);
|
|
677
|
+
const fileBlob = new Blob([fileContent]);
|
|
678
|
+
const uploadResult = await chrisIO.file_upload(fileBlob, path.dirname(file.chrisPath), path.basename(file.chrisPath));
|
|
679
|
+
if (uploadResult) {
|
|
680
|
+
summary.transferredCount++;
|
|
681
|
+
summary.transferSize += fileContent.length;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
summary.failedCount++;
|
|
685
|
+
console.log(chalk.yellow(`Failed to upload: ${file.hostPath}`));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
summary.failedCount++;
|
|
690
|
+
console.log(chalk.red(`Error uploading ${file.hostPath}: ${error instanceof Error ? error.message : String(error)}`));
|
|
691
|
+
}
|
|
692
|
+
progressBar.update(index + 1, {
|
|
693
|
+
bytes: bytes_format(summary.transferSize), // Renamed
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
const transmission = transmissionSummary_get(summary);
|
|
697
|
+
progressBar.stop();
|
|
698
|
+
summary.totalFiles = scanRecord.fileInfo.length;
|
|
699
|
+
summary.duration = transmission.duration;
|
|
700
|
+
summary.speed = transmission.speed;
|
|
701
|
+
return summary;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Downloads files from ChRIS to the local filesystem.
|
|
705
|
+
*
|
|
706
|
+
* @param options - TransferCLI options including host path.
|
|
707
|
+
* @returns A Promise resolving to true if download was successful, false otherwise.
|
|
708
|
+
*/
|
|
709
|
+
async function download_handle(options) {
|
|
710
|
+
await chrisContext.currentContext_update();
|
|
711
|
+
const folder = chrisContext.singleContext.folder;
|
|
712
|
+
if (!folder) {
|
|
713
|
+
console.error(chalk.red("No ChRIS folder context set. Use 'connect' to establish a session."));
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
const localTarget = path.resolve(options.hostpath || process.cwd());
|
|
717
|
+
try {
|
|
718
|
+
const summary = await files_downloadWithProgress(folder, localTarget, {
|
|
719
|
+
force: options.force,
|
|
720
|
+
});
|
|
721
|
+
table_display(summaryTable_create(summary), ["Metric", "Value"], {
|
|
722
|
+
title: { title: "Download summary", justification: "center" },
|
|
723
|
+
});
|
|
724
|
+
return summary.failedCount === 0;
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
728
|
+
console.error(chalk.red(`Download failed: ${msg}`));
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Uploads files from the local filesystem to ChRIS.
|
|
734
|
+
*
|
|
735
|
+
* @param options - TransferCLI options including host path.
|
|
736
|
+
* @returns A Promise resolving to true if upload was successful, false otherwise.
|
|
737
|
+
*/
|
|
738
|
+
async function upload_handle(options) {
|
|
739
|
+
const scanRecord = await localFS_scan(options);
|
|
740
|
+
if (!scanRecord) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
const progressBar = progressBar_setupAndStart(scanRecord);
|
|
744
|
+
const summary = await chris_push(scanRecord, progressBar);
|
|
745
|
+
table_display(summaryTable_create(summary), ["Metric", "Value"], {
|
|
746
|
+
title: { title: "Upload summary", justification: "center" },
|
|
747
|
+
});
|
|
748
|
+
return summary.failedCount === 0;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Formats a number of bytes into a human-readable string.
|
|
752
|
+
*
|
|
753
|
+
* @param bytes - The number of bytes.
|
|
754
|
+
* @returns A formatted string (e.g., "1.23 MB").
|
|
755
|
+
*/
|
|
756
|
+
export function bytes_format(bytes) {
|
|
757
|
+
if (bytes === 0)
|
|
758
|
+
return "0 B";
|
|
759
|
+
const k = 1024;
|
|
760
|
+
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
761
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
762
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Sets up the 'path' command for performing operations on ChRIS paths.
|
|
766
|
+
*
|
|
767
|
+
* @param program - The Commander.js program instance.
|
|
768
|
+
*/
|
|
769
|
+
export async function pathCommand_setup(program) {
|
|
770
|
+
const pathCommand = program
|
|
771
|
+
.command("path")
|
|
772
|
+
.description("Perform operations on ChRIS paths");
|
|
773
|
+
pathCommand
|
|
774
|
+
.command("upload <hostpath>")
|
|
775
|
+
.description("upload the <hostpath> from your computer into the current ChRIS folder context")
|
|
776
|
+
.action(async (hostpath) => {
|
|
777
|
+
const result = await upload_handle({ hostpath });
|
|
778
|
+
});
|
|
779
|
+
pathCommand
|
|
780
|
+
.command("download [hostpath]")
|
|
781
|
+
.description("download the current ChRIS folder context to current dir or [hostdir]")
|
|
782
|
+
.option("-f, --force", "overwrite existing local path")
|
|
783
|
+
.action(async (hostpath, options) => {
|
|
784
|
+
const result = await download_handle({
|
|
785
|
+
hostpath: hostpath || process.cwd(),
|
|
786
|
+
force: options.force,
|
|
787
|
+
});
|
|
788
|
+
console.log(result);
|
|
789
|
+
});
|
|
790
|
+
pathCommand
|
|
791
|
+
.command("scan")
|
|
792
|
+
.description("scan and list all files recursively from the current ChRIS folder context")
|
|
793
|
+
.option("--tree", "present in a style reminiscent of the UNIX tree command")
|
|
794
|
+
.option("--follow", "follow chrislinks")
|
|
795
|
+
.option("--dirsOnly", "scan only directories")
|
|
796
|
+
.option("--silent", "suppress output")
|
|
797
|
+
.option("--hostpath <path>", "specify host path for file mapping")
|
|
798
|
+
.option("--filter <text>", "filter results to include only paths containing the specified text")
|
|
799
|
+
.option("--endsWith <text>", "filter results to include only paths ending with the specified text")
|
|
800
|
+
.option("--mermaid", "render the result as a Mermaid.js diagram")
|
|
801
|
+
.option("--save <filename>", "save the Mermaid diagram to the specified file")
|
|
802
|
+
.action(async (options) => {
|
|
803
|
+
await scan_do(options);
|
|
804
|
+
});
|
|
805
|
+
}
|