colonynote 1.0.0-beta.8 → 1.0.0
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/README.md +6 -6
- package/README.zh.md +6 -6
- package/bin/colonynote.js +48 -81
- package/dist/client/assets/arc-Ezd9GmKE.js +1 -0
- package/dist/client/assets/architecture-YZFGNWBL-Dns9u0e5.js +1 -0
- package/dist/client/assets/architectureDiagram-Q4EWVU46-BmLJotd1.js +36 -0
- package/dist/client/assets/array-C0548cPn.js +1 -0
- package/dist/client/assets/blockDiagram-DXYQGD6D-ZqrNO8HM.js +132 -0
- package/dist/client/assets/c4Diagram-AHTNJAMY-BJWyymhT.js +10 -0
- package/dist/client/assets/channel-C-zEXS3V.js +1 -0
- package/dist/client/assets/chunk-2KRD3SAO-B1G2AY6H.js +1 -0
- package/dist/client/assets/chunk-4BX2VUAB-mLNpdiNK.js +1 -0
- package/dist/client/assets/chunk-4TB4RGXK-BsRi3qra.js +206 -0
- package/dist/client/assets/chunk-55IACEB6-BkCqG_Mc.js +1 -0
- package/dist/client/assets/chunk-67CJDMHE-Cz-mqdi4.js +1 -0
- package/dist/client/assets/chunk-7N4EOEYR-BFd0Oy6w.js +1 -0
- package/dist/client/assets/chunk-AA7GKIK3-D77gJjH7.js +1 -0
- package/dist/client/assets/chunk-CIAEETIT-DxQTGSBH.js +1 -0
- package/dist/client/assets/chunk-Dlc7tRH4.js +1 -0
- package/dist/client/assets/chunk-EDXVE4YY-DglU8MxC.js +1 -0
- package/dist/client/assets/{chunk-FMBD7UC4-aAnHvYnB.js → chunk-FMBD7UC4-B_1_v8GP.js} +2 -2
- package/dist/client/assets/chunk-FOC6F5B3-CEMl5H_U.js +1 -0
- package/dist/client/assets/chunk-K5T4RW27-Dzh5Xx8O.js +94 -0
- package/dist/client/assets/chunk-KGLVRYIC-Be2dMs_W.js +1 -0
- package/dist/client/assets/chunk-LIHQZDEY-BGBWPKdn.js +1 -0
- package/dist/client/assets/chunk-ORNJ4GCN-W_NT507t.js +1 -0
- package/dist/client/assets/chunk-OYMX7WX6-BV6RCL_j.js +231 -0
- package/dist/client/assets/chunk-QZHKN3VN-B0CMV9NG.js +1 -0
- package/dist/client/assets/chunk-YZCP3GAM-CDitFgw3.js +1 -0
- package/dist/client/assets/classDiagram-6PBFFD2Q-XeqY_hie.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-D5yY32qd.js +1 -0
- package/dist/client/assets/clone-AnN0418d.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-YQtZwRYb.js +1 -0
- package/dist/client/assets/cytoscape.esm-DT0IEibP.js +321 -0
- package/dist/client/assets/dagre-Cs_hK0RA.js +1 -0
- package/dist/client/assets/dagre-KV5264BT-DvMqAy8J.js +4 -0
- package/dist/client/assets/defaultLocale-ClcAPJ5U.js +1 -0
- package/dist/client/assets/diagram-5BDNPKRD-BwrN8bUG.js +10 -0
- package/dist/client/assets/diagram-G4DWMVQ6-Cqaj-jnn.js +24 -0
- package/dist/client/assets/diagram-MMDJMWI5-DNA312HK.js +43 -0
- package/dist/client/assets/diagram-TYMM5635-BujcYOHW.js +24 -0
- package/dist/client/assets/erDiagram-SMLLAGMA-ZjZZH-zN.js +85 -0
- package/dist/client/assets/flatten-DA7ZzNkq.js +1 -0
- package/dist/client/assets/flowDiagram-DWJPFMVM-CeIGHswO.js +162 -0
- package/dist/client/assets/ganttDiagram-T4ZO3ILL-BeGMFmHq.js +292 -0
- package/dist/client/assets/gitGraph-7Q5UKJZL-Dib3-KZp.js +1 -0
- package/dist/client/assets/gitGraphDiagram-UUTBAWPF-2fnUKQRp.js +106 -0
- package/dist/client/assets/graphlib-DRri47Ms.js +1 -0
- package/dist/client/assets/identity-CQcu21F9.js +1 -0
- package/dist/client/assets/index-DKyd5iCm.css +2 -0
- package/dist/client/assets/index-Ts5WhtRB.js +574 -0
- package/dist/client/assets/info-OMHHGYJF-oQ5--hGK.js +1 -0
- package/dist/client/assets/infoDiagram-42DDH7IO-KL_uq6JQ.js +2 -0
- package/dist/client/assets/init-TU6eJ00_.js +1 -0
- package/dist/client/assets/ishikawaDiagram-UXIWVN3A-DYWG2VdA.js +70 -0
- package/dist/client/assets/journeyDiagram-VCZTEJTY-DT7IVz7z.js +139 -0
- package/dist/client/assets/kanban-definition-6JOO6SKY-CHPPLj8n.js +89 -0
- package/dist/client/assets/katex-HOUACuRw.js +257 -0
- package/dist/client/assets/linear-CizMozZt.js +1 -0
- package/dist/client/assets/mermaid-parser.core-Bpqvb2jv.js +4 -0
- package/dist/client/assets/mindmap-definition-QFDTVHPH-Dov77gi8.js +96 -0
- package/dist/client/assets/ordinal-DCsgWfZW.js +1 -0
- package/dist/client/assets/packet-4T2RLAQJ-Cuyyn0tM.js +1 -0
- package/dist/client/assets/path-yo4Xej8w.js +1 -0
- package/dist/client/assets/pie-ZZUOXDRM-DajTPYmY.js +1 -0
- package/dist/client/assets/pieDiagram-DEJITSTG-DwmqzTu7.js +30 -0
- package/dist/client/assets/quadrantDiagram-34T5L4WZ-CdCBXbvb.js +7 -0
- package/dist/client/assets/radar-PYXPWWZC-DWe78GFc.js +1 -0
- package/dist/client/assets/reduce-DV5GRzs4.js +1 -0
- package/dist/client/assets/requirementDiagram-MS252O5E-32eCbPFl.js +84 -0
- package/dist/client/assets/rough.esm-DulVNktb.js +1 -0
- package/dist/client/assets/sankeyDiagram-XADWPNL6-BBDL_wPZ.js +10 -0
- package/dist/client/assets/sequenceDiagram-FGHM5R23-Ck7ukfb7.js +157 -0
- package/dist/client/assets/stateDiagram-FHFEXIEX-BKJrixhp.js +1 -0
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Bio1QBYF.js +1 -0
- package/dist/client/assets/timeline-definition-GMOUNBTQ-B_D-q7Kc.js +120 -0
- package/dist/client/assets/treeView-SZITEDCU-BQ27_5HI.js +1 -0
- package/dist/client/assets/treemap-W4RFUUIX-D2TpEq--.js +1 -0
- package/dist/client/assets/vennDiagram-DHZGUBPP-CvHqv3h2.js +34 -0
- package/dist/client/assets/wardley-RL74JXVD-DoAmhKVb.js +1 -0
- package/dist/client/assets/wardleyDiagram-NUSXRM2D-CYRpN4xk.js +20 -0
- package/dist/client/assets/xychartDiagram-5P7HB3ND-Dh0MaaMp.js +7 -0
- package/dist/client/index.html +7 -2
- package/dist/config.js +122 -52
- package/dist/server/api.js +637 -31
- package/dist/server/ignore.js +221 -0
- package/dist/server/index.js +78 -24
- package/dist/server/search-ignore.test.js +51 -0
- package/dist/server/watcher.js +54 -18
- package/package.json +16 -16
- package/dist/client/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/dist/client/assets/_basePickBy-COW6_o5l.js +0 -1
- package/dist/client/assets/_baseUniq-BZLZ0Cq5.js +0 -1
- package/dist/client/assets/arc-ChrJd4LX.js +0 -1
- package/dist/client/assets/architectureDiagram-2XIMDMQ5-DUvtAUqm.js +0 -36
- package/dist/client/assets/blockDiagram-WCTKOSBZ-CZjHlzLw.js +0 -132
- package/dist/client/assets/c4Diagram-IC4MRINW-Bb4QcjBn.js +0 -10
- package/dist/client/assets/channel-Kq606-yO.js +0 -1
- package/dist/client/assets/chunk-4BX2VUAB-U2gnuzht.js +0 -1
- package/dist/client/assets/chunk-55IACEB6-DDf5FyZA.js +0 -1
- package/dist/client/assets/chunk-JSJVCQXG-95g2NCsz.js +0 -1
- package/dist/client/assets/chunk-KX2RTZJC-Uw77DOxq.js +0 -1
- package/dist/client/assets/chunk-NQ4KR5QH-CEtk7AxS.js +0 -220
- package/dist/client/assets/chunk-QZHKN3VN-CxHNihbm.js +0 -1
- package/dist/client/assets/chunk-WL4C6EOR-YWnktay4.js +0 -189
- package/dist/client/assets/classDiagram-VBA2DB6C-Bgw-akZn.js +0 -1
- package/dist/client/assets/classDiagram-v2-RAHNMMFH-Bgw-akZn.js +0 -1
- package/dist/client/assets/clone-DXEvmIA_.js +0 -1
- package/dist/client/assets/cose-bilkent-S5V4N54A-BcT_Pp_t.js +0 -1
- package/dist/client/assets/cytoscape.esm-BQaXIfA_.js +0 -331
- package/dist/client/assets/dagre-KLK3FWXG-DfkYqpYp.js +0 -4
- package/dist/client/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/dist/client/assets/diagram-E7M64L7V-kzGsyL8B.js +0 -24
- package/dist/client/assets/diagram-IFDJBPK2-DK7bXX4F.js +0 -43
- package/dist/client/assets/diagram-P4PSJMXO-z9xCBdYx.js +0 -24
- package/dist/client/assets/erDiagram-INFDFZHY-D7a5C5o_.js +0 -70
- package/dist/client/assets/flowDiagram-PKNHOUZH-DqBsAGYI.js +0 -162
- package/dist/client/assets/ganttDiagram-A5KZAMGK-CytDHso0.js +0 -292
- package/dist/client/assets/gitGraphDiagram-K3NZZRJ6-C_xN53WG.js +0 -65
- package/dist/client/assets/graph-CZtfE55r.js +0 -1
- package/dist/client/assets/index-AcpT_uDS.css +0 -1
- package/dist/client/assets/index-Ci71-3A2.js +0 -705
- package/dist/client/assets/infoDiagram-LFFYTUFH-JmJVtb4-.js +0 -2
- package/dist/client/assets/init-Gi6I4Gst.js +0 -1
- package/dist/client/assets/ishikawaDiagram-PHBUUO56-BZLBgU5l.js +0 -70
- package/dist/client/assets/journeyDiagram-4ABVD52K-Bso62bBS.js +0 -139
- package/dist/client/assets/kanban-definition-K7BYSVSG-D9jsFbIS.js +0 -89
- package/dist/client/assets/katex-B1X10hvy.js +0 -261
- package/dist/client/assets/layout-Db-5vCEZ.js +0 -1
- package/dist/client/assets/linear-DdXT-poy.js +0 -1
- package/dist/client/assets/mindmap-definition-YRQLILUH-DUpkjOn4.js +0 -68
- package/dist/client/assets/ordinal-Cboi1Yqb.js +0 -1
- package/dist/client/assets/pieDiagram-SKSYHLDU-BTseg5zx.js +0 -30
- package/dist/client/assets/quadrantDiagram-337W2JSQ-Cf86XD1_.js +0 -7
- package/dist/client/assets/requirementDiagram-Z7DCOOCP-BK2yRQcz.js +0 -73
- package/dist/client/assets/sankeyDiagram-WA2Y5GQK-jBbEn6rj.js +0 -10
- package/dist/client/assets/sequenceDiagram-2WXFIKYE-qg69rBRP.js +0 -145
- package/dist/client/assets/stateDiagram-RAJIS63D-BFIxDKEY.js +0 -1
- package/dist/client/assets/stateDiagram-v2-FVOUBMTO-DYqLkNfp.js +0 -1
- package/dist/client/assets/timeline-definition-YZTLITO2-B17QFyRY.js +0 -61
- package/dist/client/assets/treemap-KZPCXAKY-CGSXpgHF.js +0 -162
- package/dist/client/assets/vennDiagram-LZ73GAT5-mbh453Lq.js +0 -34
- package/dist/client/assets/xychartDiagram-JWTSCODW-COWgeBYx.js +0 -7
- package/dist/server/app.js +0 -14
package/dist/server/api.js
CHANGED
|
@@ -1,45 +1,100 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
3
4
|
import path from 'path';
|
|
4
|
-
import
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { saveConfig, DEFAULT_SENSITIVE_PATHS } from '../config.js';
|
|
8
|
+
import { minimatch } from 'minimatch';
|
|
9
|
+
import fuzzysort from 'fuzzysort';
|
|
5
10
|
function isAllowed(pathStr, config) {
|
|
6
11
|
const resolved = path.resolve(pathStr);
|
|
7
|
-
return
|
|
12
|
+
return config.dirs.some(dir => {
|
|
13
|
+
const dirPath = path.resolve(dir.path);
|
|
14
|
+
return resolved === dirPath || resolved.startsWith(dirPath + path.sep);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function validateRoot(dirPath, config) {
|
|
18
|
+
const resolved = path.resolve(dirPath);
|
|
19
|
+
const dir = config.dirs.find(r => path.resolve(r.path) === resolved);
|
|
20
|
+
return dir ? path.resolve(dir.path) : null;
|
|
21
|
+
}
|
|
22
|
+
function checkSensitivePath(inputPath) {
|
|
23
|
+
const basename = path.basename(inputPath);
|
|
24
|
+
for (const pattern of DEFAULT_SENSITIVE_PATHS) {
|
|
25
|
+
if (minimatch(basename, pattern, { nocase: true, dot: true }))
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function checkNestedPath(newPath, existingDirs) {
|
|
31
|
+
const resolved = path.resolve(newPath);
|
|
32
|
+
for (const dir of existingDirs) {
|
|
33
|
+
const existing = path.resolve(dir.path);
|
|
34
|
+
if (resolved === existing)
|
|
35
|
+
return { isNested: true, conflictWith: dir.path, reason: 'duplicate' };
|
|
36
|
+
if (resolved.startsWith(existing + path.sep))
|
|
37
|
+
return { isNested: true, conflictWith: dir.path, reason: 'child' };
|
|
38
|
+
if (existing.startsWith(resolved + path.sep))
|
|
39
|
+
return { isNested: true, conflictWith: dir.path, reason: 'parent' };
|
|
40
|
+
}
|
|
41
|
+
return { isNested: false };
|
|
42
|
+
}
|
|
43
|
+
function findRootForPath(filePath, config) {
|
|
44
|
+
for (const dir of config.dirs) {
|
|
45
|
+
const dirPath = path.resolve(dir.path);
|
|
46
|
+
const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
47
|
+
const fullPath = path.join(dirPath, relativePath);
|
|
48
|
+
if ((fullPath.startsWith(dirPath + path.sep) || fullPath === dirPath) && existsSync(fullPath)) {
|
|
49
|
+
return dirPath;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
8
53
|
}
|
|
9
54
|
function hasAllowedExtension(filename, extensions) {
|
|
10
55
|
const ext = path.extname(filename).toLowerCase();
|
|
11
56
|
return extensions.includes(ext);
|
|
12
57
|
}
|
|
13
|
-
async function walkDirectory(dir, config) {
|
|
58
|
+
async function walkDirectory(dir, dirPath, config, matcher) {
|
|
14
59
|
const nodes = [];
|
|
15
60
|
try {
|
|
16
61
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
17
62
|
for (const entry of entries) {
|
|
63
|
+
const fullPath = path.join(dir, entry.name);
|
|
64
|
+
const isDir = entry.isDirectory();
|
|
18
65
|
if (!config.showHiddenFiles && entry.name.startsWith('.'))
|
|
19
66
|
continue;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
67
|
+
if (matcher.isIgnored(fullPath, isDir))
|
|
68
|
+
continue;
|
|
69
|
+
if (isDir) {
|
|
70
|
+
const children = await walkDirectory(fullPath, dirPath, config, matcher);
|
|
71
|
+
const relativePath = path.relative(dirPath, fullPath).replace(/\\/g, '/');
|
|
23
72
|
nodes.push({
|
|
24
73
|
name: entry.name,
|
|
25
|
-
path:
|
|
74
|
+
path: relativePath ? '/' + relativePath : '/',
|
|
26
75
|
type: 'directory',
|
|
76
|
+
rootPath: dirPath,
|
|
27
77
|
children,
|
|
28
78
|
});
|
|
29
79
|
}
|
|
30
80
|
else if (entry.isFile()) {
|
|
31
81
|
if (hasAllowedExtension(entry.name, config.allowedExtensions)) {
|
|
82
|
+
const relativePath = path.relative(dirPath, fullPath).replace(/\\/g, '/');
|
|
32
83
|
nodes.push({
|
|
33
84
|
name: entry.name,
|
|
34
|
-
path:
|
|
85
|
+
path: relativePath ? '/' + relativePath : '/',
|
|
35
86
|
type: 'file',
|
|
87
|
+
rootPath: dirPath,
|
|
36
88
|
});
|
|
37
89
|
}
|
|
38
90
|
}
|
|
39
91
|
}
|
|
40
92
|
}
|
|
41
93
|
catch (e) {
|
|
42
|
-
// ignore
|
|
94
|
+
// Re-throw error if this is the root directory, otherwise ignore
|
|
95
|
+
if (dir === dirPath) {
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
43
98
|
}
|
|
44
99
|
nodes.sort((a, b) => {
|
|
45
100
|
if (a.type === 'directory' && b.type === 'file')
|
|
@@ -50,48 +105,366 @@ async function walkDirectory(dir, config) {
|
|
|
50
105
|
});
|
|
51
106
|
return nodes;
|
|
52
107
|
}
|
|
53
|
-
export function
|
|
108
|
+
export function createMutableConfigHolder(initialConfig, initialMatcher) {
|
|
109
|
+
let config = initialConfig;
|
|
110
|
+
let matcher = initialMatcher;
|
|
111
|
+
return {
|
|
112
|
+
get config() { return config; },
|
|
113
|
+
get matcher() { return matcher; },
|
|
114
|
+
setConfig(c) { config = c; },
|
|
115
|
+
setMatcher(m) { matcher = m; },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function createFileRouter(holder, env = 'production') {
|
|
54
119
|
const router = new Hono();
|
|
120
|
+
function getConfig() { return holder.config; }
|
|
121
|
+
function getMatcher() { return holder.matcher; }
|
|
122
|
+
let treeCache = null;
|
|
123
|
+
const CACHE_TTL_MS = 3000;
|
|
124
|
+
function computeConfigHash() {
|
|
125
|
+
const config = getConfig();
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
dirs: config.dirs.map(d => ({ path: d.path, name: d.name })),
|
|
128
|
+
showHiddenFiles: config.showHiddenFiles,
|
|
129
|
+
allowedExtensions: config.allowedExtensions,
|
|
130
|
+
ignore: config.ignore,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function invalidateTreeCache() {
|
|
134
|
+
treeCache = null;
|
|
135
|
+
}
|
|
136
|
+
async function getFileGroups() {
|
|
137
|
+
const config = getConfig();
|
|
138
|
+
const matcher = getMatcher();
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
const hash = computeConfigHash();
|
|
141
|
+
if (treeCache && (now - treeCache.timestamp) < CACHE_TTL_MS && treeCache.configHash === hash) {
|
|
142
|
+
return treeCache.groups;
|
|
143
|
+
}
|
|
144
|
+
const groups = await Promise.all(config.dirs.map(async (dir) => {
|
|
145
|
+
try {
|
|
146
|
+
return {
|
|
147
|
+
root: dir,
|
|
148
|
+
files: await walkDirectory(dir.path, dir.path, config, matcher)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
return {
|
|
153
|
+
root: dir,
|
|
154
|
+
files: [],
|
|
155
|
+
error: e instanceof Error ? e.message : 'Failed to read directory'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
treeCache = { groups, timestamp: now, configHash: hash };
|
|
160
|
+
return groups;
|
|
161
|
+
}
|
|
55
162
|
router.get('/config', async (c) => {
|
|
163
|
+
const config = getConfig();
|
|
56
164
|
return c.json({
|
|
57
165
|
showHiddenFiles: config.showHiddenFiles,
|
|
58
166
|
allowedExtensions: config.allowedExtensions,
|
|
167
|
+
ignore: config.ignore,
|
|
59
168
|
});
|
|
60
169
|
});
|
|
61
170
|
router.patch('/config', async (c) => {
|
|
171
|
+
const config = getConfig();
|
|
172
|
+
const matcher = getMatcher();
|
|
62
173
|
try {
|
|
63
174
|
const body = await c.req.json();
|
|
64
|
-
const allowedFields = ['showHiddenFiles', 'allowedExtensions'];
|
|
175
|
+
const allowedFields = ['showHiddenFiles', 'allowedExtensions', 'ignore'];
|
|
65
176
|
const updates = {};
|
|
66
177
|
for (const key of allowedFields) {
|
|
67
178
|
if (key in body) {
|
|
68
179
|
updates[key] = body[key];
|
|
69
180
|
}
|
|
70
181
|
}
|
|
71
|
-
|
|
182
|
+
saveConfig(config, env);
|
|
183
|
+
invalidateTreeCache();
|
|
72
184
|
if (typeof updates.showHiddenFiles === 'boolean') {
|
|
73
185
|
config.showHiddenFiles = updates.showHiddenFiles;
|
|
74
186
|
}
|
|
75
187
|
if (Array.isArray(updates.allowedExtensions)) {
|
|
76
188
|
config.allowedExtensions = updates.allowedExtensions;
|
|
77
189
|
}
|
|
78
|
-
|
|
190
|
+
if (updates.ignore) {
|
|
191
|
+
if (Array.isArray(updates.ignore.patterns)) {
|
|
192
|
+
config.ignore.patterns = updates.ignore.patterns;
|
|
193
|
+
matcher.updateGlobalPatterns(updates.ignore.patterns);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return c.json({ success: true, config: { showHiddenFiles: config.showHiddenFiles, allowedExtensions: config.allowedExtensions, ignore: config.ignore } });
|
|
79
197
|
}
|
|
80
198
|
catch (e) {
|
|
81
199
|
console.error('Failed to update config:', e);
|
|
82
200
|
return c.json({ error: 'Failed to update config' }, 500);
|
|
83
201
|
}
|
|
84
202
|
});
|
|
203
|
+
// Dir management routes
|
|
204
|
+
router.get('/dirs', async (c) => {
|
|
205
|
+
const config = getConfig();
|
|
206
|
+
return c.json({ dirs: config.dirs });
|
|
207
|
+
});
|
|
208
|
+
router.post('/dirs', async (c) => {
|
|
209
|
+
const config = getConfig();
|
|
210
|
+
try {
|
|
211
|
+
const body = await c.req.json();
|
|
212
|
+
const newPath = body.path;
|
|
213
|
+
if (!newPath)
|
|
214
|
+
return c.json({ error: 'Path is required' }, 400);
|
|
215
|
+
try {
|
|
216
|
+
const stat = await fs.stat(newPath);
|
|
217
|
+
if (!stat.isDirectory())
|
|
218
|
+
return c.json({ error: 'Path must be a directory' }, 400);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return c.json({ error: 'Path does not exist' }, 400);
|
|
222
|
+
}
|
|
223
|
+
if (checkSensitivePath(newPath))
|
|
224
|
+
return c.json({ error: 'Sensitive path not allowed' }, 400);
|
|
225
|
+
const nested = checkNestedPath(newPath, config.dirs);
|
|
226
|
+
if (nested.isNested)
|
|
227
|
+
return c.json({ error: 'Nested path not allowed', conflictWith: nested.conflictWith, reason: nested.reason }, 400);
|
|
228
|
+
const newDir = { path: path.resolve(newPath), exclude: body.exclude, name: body.name };
|
|
229
|
+
config.dirs.push(newDir);
|
|
230
|
+
saveConfig(config, env);
|
|
231
|
+
invalidateTreeCache();
|
|
232
|
+
return c.json({ success: true, dir: newDir });
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
return c.json({ error: 'Failed to add dir' }, 500);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
router.delete('/dirs', async (c) => {
|
|
239
|
+
const config = getConfig();
|
|
240
|
+
const pathParam = c.req.query('path');
|
|
241
|
+
if (!pathParam)
|
|
242
|
+
return c.json({ error: 'path parameter required' }, 400);
|
|
243
|
+
const idx = config.dirs.findIndex(r => path.resolve(r.path) === path.resolve(pathParam));
|
|
244
|
+
if (idx === -1)
|
|
245
|
+
return c.json({ error: 'Dir not found' }, 404);
|
|
246
|
+
config.dirs.splice(idx, 1);
|
|
247
|
+
saveConfig(config, env);
|
|
248
|
+
invalidateTreeCache();
|
|
249
|
+
return c.json({ success: true });
|
|
250
|
+
});
|
|
251
|
+
router.patch('/dirs', async (c) => {
|
|
252
|
+
const config = getConfig();
|
|
253
|
+
try {
|
|
254
|
+
const body = await c.req.json();
|
|
255
|
+
const { path: dirPath, exclude, name } = body;
|
|
256
|
+
if (!dirPath)
|
|
257
|
+
return c.json({ error: 'Path is required' }, 400);
|
|
258
|
+
const dir = config.dirs.find(r => path.resolve(r.path) === path.resolve(dirPath));
|
|
259
|
+
if (!dir)
|
|
260
|
+
return c.json({ error: 'Dir not found' }, 404);
|
|
261
|
+
if (exclude !== undefined)
|
|
262
|
+
dir.exclude = exclude;
|
|
263
|
+
if (name !== undefined)
|
|
264
|
+
dir.name = name;
|
|
265
|
+
saveConfig(config, env);
|
|
266
|
+
invalidateTreeCache();
|
|
267
|
+
return c.json({ success: true, dir });
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
return c.json({ error: 'Failed to update dir' }, 500);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
router.get('/dirs/browse', async (c) => {
|
|
274
|
+
const rawPath = c.req.query('path') || '';
|
|
275
|
+
if (!rawPath.trim())
|
|
276
|
+
return c.json({ dirs: [], currentPath: '' });
|
|
277
|
+
let resolvedPath;
|
|
278
|
+
if (rawPath === '~' || rawPath.startsWith('~/')) {
|
|
279
|
+
resolvedPath = path.join(os.homedir(), rawPath.slice(1));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
resolvedPath = path.resolve(rawPath);
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const stat = await fs.stat(resolvedPath);
|
|
286
|
+
if (!stat.isDirectory()) {
|
|
287
|
+
return c.json({ dirs: [], currentPath: rawPath });
|
|
288
|
+
}
|
|
289
|
+
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
290
|
+
const dirs = [];
|
|
291
|
+
for (const entry of entries) {
|
|
292
|
+
if (entry.name.startsWith('.'))
|
|
293
|
+
continue;
|
|
294
|
+
const fullPath = path.join(resolvedPath, entry.name);
|
|
295
|
+
if (entry.isDirectory() && !checkSensitivePath(fullPath)) {
|
|
296
|
+
dirs.push(fullPath);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
dirs.sort((a, b) => path.basename(a).localeCompare(path.basename(b)));
|
|
300
|
+
return c.json({ dirs, currentPath: rawPath });
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return c.json({ dirs: [], currentPath: rawPath });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
router.get('/dirs/search', async (c) => {
|
|
307
|
+
const config = getConfig();
|
|
308
|
+
const query = c.req.query('q') || '';
|
|
309
|
+
const rawRoot = c.req.query('root');
|
|
310
|
+
const mode = c.req.query('mode') || 'fuzzy';
|
|
311
|
+
let searchRoot;
|
|
312
|
+
if (rawRoot === '~' || rawRoot === '') {
|
|
313
|
+
searchRoot = os.homedir();
|
|
314
|
+
}
|
|
315
|
+
else if (rawRoot === '/') {
|
|
316
|
+
searchRoot = '/';
|
|
317
|
+
}
|
|
318
|
+
else if (rawRoot && rawRoot.startsWith('~/')) {
|
|
319
|
+
searchRoot = path.join(os.homedir(), rawRoot.slice(2));
|
|
320
|
+
}
|
|
321
|
+
else if (rawRoot && path.isAbsolute(rawRoot)) {
|
|
322
|
+
searchRoot = path.normalize(rawRoot);
|
|
323
|
+
}
|
|
324
|
+
else if (rawRoot) {
|
|
325
|
+
// Relative path like 'projects' → resolve relative to home
|
|
326
|
+
searchRoot = path.join(os.homedir(), rawRoot);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
searchRoot = os.homedir();
|
|
330
|
+
}
|
|
331
|
+
// Browse mode: list direct children directories
|
|
332
|
+
if (mode === 'browse') {
|
|
333
|
+
try {
|
|
334
|
+
const stat = await fs.stat(searchRoot);
|
|
335
|
+
if (!stat.isDirectory()) {
|
|
336
|
+
return c.json({ matches: [] });
|
|
337
|
+
}
|
|
338
|
+
const entries = await fs.readdir(searchRoot, { withFileTypes: true });
|
|
339
|
+
const dirs = [];
|
|
340
|
+
for (const entry of entries) {
|
|
341
|
+
if (entry.name.startsWith('.'))
|
|
342
|
+
continue;
|
|
343
|
+
const fullPath = path.join(searchRoot, entry.name);
|
|
344
|
+
if (entry.isDirectory() && !checkSensitivePath(fullPath)) {
|
|
345
|
+
dirs.push({ path: fullPath, score: 0, indexes: [] });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
dirs.sort((a, b) => path.basename(a.path).localeCompare(path.basename(b.path)));
|
|
349
|
+
return c.json({ matches: dirs });
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return c.json({ matches: [] });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Search mode requires non-empty query
|
|
356
|
+
if (!query.trim())
|
|
357
|
+
return c.json({ matches: [] });
|
|
358
|
+
const MAX_DEPTH = 5;
|
|
359
|
+
const MAX_CANDIDATES = 10000;
|
|
360
|
+
const MAX_RESULTS = 100;
|
|
361
|
+
const candidates = [];
|
|
362
|
+
async function traverse(dir, depth) {
|
|
363
|
+
if (depth > MAX_DEPTH || candidates.length >= MAX_CANDIDATES)
|
|
364
|
+
return;
|
|
365
|
+
try {
|
|
366
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
367
|
+
const subDirs = [];
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
if (entry.name.startsWith('.'))
|
|
370
|
+
continue;
|
|
371
|
+
const fullPath = path.join(dir, entry.name);
|
|
372
|
+
// Check if path matches any ignore pattern
|
|
373
|
+
const isIgnored = config.ignore?.patterns?.some(pattern => {
|
|
374
|
+
const normalizedPattern = pattern.endsWith('/') ? pattern.slice(0, -1) : pattern;
|
|
375
|
+
return minimatch(fullPath, normalizedPattern, { dot: true, matchBase: true });
|
|
376
|
+
});
|
|
377
|
+
if (isIgnored)
|
|
378
|
+
continue;
|
|
379
|
+
if (entry.isDirectory() && !checkSensitivePath(fullPath)) {
|
|
380
|
+
subDirs.push(fullPath);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
for (const subDir of subDirs) {
|
|
384
|
+
if (candidates.length >= MAX_CANDIDATES)
|
|
385
|
+
break;
|
|
386
|
+
candidates.push({ name: path.basename(subDir), fullPath: subDir });
|
|
387
|
+
}
|
|
388
|
+
for (const subDir of subDirs) {
|
|
389
|
+
if (candidates.length >= MAX_CANDIDATES)
|
|
390
|
+
break;
|
|
391
|
+
await traverse(subDir, depth + 1);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch { }
|
|
395
|
+
}
|
|
396
|
+
await traverse(searchRoot, 0);
|
|
397
|
+
let results;
|
|
398
|
+
if (mode === 'prefix') {
|
|
399
|
+
const lowerQuery = query.toLowerCase();
|
|
400
|
+
results = candidates
|
|
401
|
+
.filter(c => c.name.toLowerCase().startsWith(lowerQuery))
|
|
402
|
+
.slice(0, MAX_RESULTS)
|
|
403
|
+
.map(c => ({
|
|
404
|
+
obj: c,
|
|
405
|
+
score: 0,
|
|
406
|
+
indexes: Array.from({ length: query.length }, (_, i) => i)
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
results = fuzzysort.go(query, candidates, {
|
|
411
|
+
key: 'name',
|
|
412
|
+
limit: MAX_RESULTS,
|
|
413
|
+
threshold: 0.5,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const matches = results.map(r => ({
|
|
417
|
+
path: r.obj.fullPath,
|
|
418
|
+
score: r.score,
|
|
419
|
+
indexes: r.indexes,
|
|
420
|
+
}));
|
|
421
|
+
return c.json({ matches });
|
|
422
|
+
});
|
|
423
|
+
// Lazy-load directory children on demand (performance optimization)
|
|
424
|
+
router.get('/children', async (c) => {
|
|
425
|
+
const config = getConfig();
|
|
426
|
+
const matcher = getMatcher();
|
|
427
|
+
const dirPathParam = c.req.query('dirPath');
|
|
428
|
+
const rootParam = c.req.query('root');
|
|
429
|
+
if (!dirPathParam)
|
|
430
|
+
return c.json({ error: 'dirPath is required' }, 400);
|
|
431
|
+
let dirPath;
|
|
432
|
+
if (rootParam) {
|
|
433
|
+
dirPath = validateRoot(rootParam, config);
|
|
434
|
+
if (!dirPath)
|
|
435
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
dirPath = findRootForPath(dirPathParam, config);
|
|
439
|
+
if (!dirPath)
|
|
440
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
441
|
+
}
|
|
442
|
+
const relativePath = dirPathParam.startsWith('/') ? dirPathParam.slice(1) : dirPathParam;
|
|
443
|
+
const fullPath = path.join(dirPath, relativePath);
|
|
444
|
+
if (!isAllowed(fullPath, config))
|
|
445
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
446
|
+
try {
|
|
447
|
+
const stat = await fs.stat(fullPath);
|
|
448
|
+
if (!stat.isDirectory())
|
|
449
|
+
return c.json({ error: 'Not a directory' }, 400);
|
|
450
|
+
const children = await walkDirectory(fullPath, dirPath, config, matcher);
|
|
451
|
+
return c.json({ children });
|
|
452
|
+
}
|
|
453
|
+
catch (e) {
|
|
454
|
+
return c.json({ error: 'Failed to read directory' }, 500);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
85
457
|
router.get('/', async (c) => {
|
|
86
458
|
try {
|
|
87
|
-
const
|
|
88
|
-
return c.json({
|
|
459
|
+
const groups = await getFileGroups();
|
|
460
|
+
return c.json({ groups });
|
|
89
461
|
}
|
|
90
462
|
catch (e) {
|
|
91
463
|
return c.json({ error: 'Failed to read directory' }, 500);
|
|
92
464
|
}
|
|
93
465
|
});
|
|
94
466
|
router.get('/content', async (c) => {
|
|
467
|
+
const config = getConfig();
|
|
95
468
|
const pathsParam = c.req.query('paths');
|
|
96
469
|
if (!pathsParam) {
|
|
97
470
|
return c.json({ error: 'paths parameter is required' }, 400);
|
|
@@ -99,7 +472,21 @@ export function createFileRouter(config) {
|
|
|
99
472
|
const paths = pathsParam.split(',').filter(Boolean);
|
|
100
473
|
const results = [];
|
|
101
474
|
for (const filePath of paths) {
|
|
102
|
-
const
|
|
475
|
+
const rootParam = c.req.query('root');
|
|
476
|
+
let dirPath;
|
|
477
|
+
if (rootParam) {
|
|
478
|
+
dirPath = path.resolve(rootParam);
|
|
479
|
+
if (!config.dirs.some(r => path.resolve(r.path) === dirPath)) {
|
|
480
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
dirPath = findRootForPath(filePath, config);
|
|
485
|
+
}
|
|
486
|
+
if (!dirPath)
|
|
487
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
488
|
+
const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
489
|
+
const fullPath = path.join(dirPath, relativePath);
|
|
103
490
|
if (!isAllowed(fullPath, config)) {
|
|
104
491
|
continue;
|
|
105
492
|
}
|
|
@@ -120,16 +507,132 @@ export function createFileRouter(config) {
|
|
|
120
507
|
}
|
|
121
508
|
return c.json({ files: results });
|
|
122
509
|
});
|
|
510
|
+
router.get('/search', async (c) => {
|
|
511
|
+
const config = getConfig();
|
|
512
|
+
const query = c.req.query('q');
|
|
513
|
+
if (!query || !query.trim()) {
|
|
514
|
+
return c.json({ results: [] });
|
|
515
|
+
}
|
|
516
|
+
const limit = parseInt(c.req.query('limit') || '50', 10);
|
|
517
|
+
const rootParam = c.req.query('root');
|
|
518
|
+
let targetDirs;
|
|
519
|
+
if (rootParam) {
|
|
520
|
+
const validatedRoot = validateRoot(rootParam, config);
|
|
521
|
+
if (!validatedRoot) {
|
|
522
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
523
|
+
}
|
|
524
|
+
targetDirs = config.dirs.filter(d => path.resolve(d.path) === validatedRoot);
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
targetDirs = config.dirs;
|
|
528
|
+
}
|
|
529
|
+
const results = [];
|
|
530
|
+
for (const dir of targetDirs) {
|
|
531
|
+
if (results.length >= limit)
|
|
532
|
+
break;
|
|
533
|
+
const dirPath = path.resolve(dir.path);
|
|
534
|
+
const dirName = dir.name || path.basename(dirPath);
|
|
535
|
+
try {
|
|
536
|
+
const { stdout, stderr } = await new Promise((resolve, reject) => {
|
|
537
|
+
execFile('rg', [
|
|
538
|
+
'--json',
|
|
539
|
+
'--ignore-case',
|
|
540
|
+
'--max-count', String(Math.min(limit, 5)),
|
|
541
|
+
'--line-number',
|
|
542
|
+
'--context', '1',
|
|
543
|
+
'-g', '*.md',
|
|
544
|
+
query,
|
|
545
|
+
dirPath,
|
|
546
|
+
], { timeout: 5000, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
547
|
+
if (error && error.code !== 1) {
|
|
548
|
+
reject(error);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
resolve({ stdout, stderr });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
if (stderr.includes('no matches'))
|
|
556
|
+
continue;
|
|
557
|
+
let currentPath = '';
|
|
558
|
+
let currentRelativePath = '';
|
|
559
|
+
let lines = [];
|
|
560
|
+
for (const rawLine of stdout.split('\n')) {
|
|
561
|
+
if (!rawLine.trim())
|
|
562
|
+
continue;
|
|
563
|
+
try {
|
|
564
|
+
const obj = JSON.parse(rawLine);
|
|
565
|
+
if (obj.type === 'begin') {
|
|
566
|
+
currentPath = obj.data.path.text;
|
|
567
|
+
currentRelativePath = path.relative(dirPath, currentPath).replace(/\\/g, '/');
|
|
568
|
+
lines = [];
|
|
569
|
+
}
|
|
570
|
+
else if (obj.type === 'match') {
|
|
571
|
+
const lineText = obj.data.lines.text;
|
|
572
|
+
lines.push({ line: lineText, lineNumber: obj.data.line_number, subMatches: obj.data.submatches });
|
|
573
|
+
}
|
|
574
|
+
else if (obj.type === 'end') {
|
|
575
|
+
if (lines.length > 0) {
|
|
576
|
+
const firstMatch = lines[0];
|
|
577
|
+
const snippet = firstMatch.line.trim();
|
|
578
|
+
results.push({
|
|
579
|
+
path: '/' + currentRelativePath,
|
|
580
|
+
name: path.basename(currentRelativePath),
|
|
581
|
+
rootPath: dirPath,
|
|
582
|
+
rootName: dirName,
|
|
583
|
+
matchedLine: snippet,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
// skip parse errors
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// skip directories where rg fails
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return c.json({ results: results.slice(0, limit) });
|
|
598
|
+
});
|
|
123
599
|
router.get('/*', async (c) => {
|
|
600
|
+
const config = getConfig();
|
|
601
|
+
const matcher = getMatcher();
|
|
124
602
|
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
125
|
-
|
|
603
|
+
// Handle root path - return grouped file tree
|
|
604
|
+
if (filePath === '/' || filePath === '') {
|
|
605
|
+
try {
|
|
606
|
+
const groups = await getFileGroups();
|
|
607
|
+
return c.json({ groups });
|
|
608
|
+
}
|
|
609
|
+
catch (e) {
|
|
610
|
+
return c.json({ error: 'Failed to read directory' }, 500);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Handle root parameter from query string
|
|
614
|
+
const rootParam = c.req.query('root');
|
|
615
|
+
let dirPath;
|
|
616
|
+
if (rootParam) {
|
|
617
|
+
dirPath = path.resolve(rootParam);
|
|
618
|
+
if (!config.dirs.some(r => path.resolve(r.path) === dirPath)) {
|
|
619
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
dirPath = findRootForPath(filePath, config);
|
|
624
|
+
}
|
|
625
|
+
if (!dirPath)
|
|
626
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
627
|
+
const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
628
|
+
const fullPath = path.join(dirPath, relativePath);
|
|
126
629
|
if (!isAllowed(fullPath, config)) {
|
|
127
630
|
return c.json({ error: 'Access denied' }, 403);
|
|
128
631
|
}
|
|
129
632
|
try {
|
|
130
633
|
const stat = await fs.stat(fullPath);
|
|
131
634
|
if (stat.isDirectory()) {
|
|
132
|
-
const files = await walkDirectory(fullPath, config);
|
|
635
|
+
const files = await walkDirectory(fullPath, dirPath, config, matcher);
|
|
133
636
|
return c.json({ files });
|
|
134
637
|
}
|
|
135
638
|
}
|
|
@@ -144,9 +647,64 @@ export function createFileRouter(config) {
|
|
|
144
647
|
return c.json({ error: 'File not found' }, 404);
|
|
145
648
|
}
|
|
146
649
|
});
|
|
650
|
+
router.post('/copy', async (c) => {
|
|
651
|
+
const config = getConfig();
|
|
652
|
+
try {
|
|
653
|
+
const body = await c.req.json();
|
|
654
|
+
let { sourcePath, targetPath, sourceRoot, targetRoot } = body;
|
|
655
|
+
if (!sourcePath || !targetPath) {
|
|
656
|
+
return c.json({ error: 'sourcePath and targetPath required' }, 400);
|
|
657
|
+
}
|
|
658
|
+
if (!sourceRoot || !targetRoot) {
|
|
659
|
+
return c.json({ error: 'sourceRoot and targetRoot required' }, 400);
|
|
660
|
+
}
|
|
661
|
+
const validatedSourceRoot = validateRoot(sourceRoot, config);
|
|
662
|
+
const validatedTargetRoot = validateRoot(targetRoot, config);
|
|
663
|
+
if (!validatedSourceRoot)
|
|
664
|
+
return c.json({ error: 'Invalid source root' }, 400);
|
|
665
|
+
if (!validatedTargetRoot)
|
|
666
|
+
return c.json({ error: 'Invalid target root' }, 400);
|
|
667
|
+
const srcFullPath = path.join(validatedSourceRoot, sourcePath);
|
|
668
|
+
let tgtFullPath = path.join(validatedTargetRoot, targetPath);
|
|
669
|
+
const stat = await fs.stat(srcFullPath);
|
|
670
|
+
const targetExists = await fs.access(tgtFullPath).then(() => true).catch(() => false);
|
|
671
|
+
if (targetExists) {
|
|
672
|
+
const ext = path.extname(targetPath);
|
|
673
|
+
const base = path.basename(targetPath, ext);
|
|
674
|
+
const dir = path.dirname(targetPath);
|
|
675
|
+
targetPath = `${dir}/${base} (copy)${ext}`;
|
|
676
|
+
tgtFullPath = path.join(validatedTargetRoot, targetPath);
|
|
677
|
+
}
|
|
678
|
+
if (stat.isDirectory()) {
|
|
679
|
+
await fs.cp(srcFullPath, tgtFullPath, { recursive: true });
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
await fs.copyFile(srcFullPath, tgtFullPath);
|
|
683
|
+
}
|
|
684
|
+
return c.json({ success: true, newPath: targetPath });
|
|
685
|
+
}
|
|
686
|
+
catch (e) {
|
|
687
|
+
console.error('Copy error:', e);
|
|
688
|
+
return c.json({ error: 'Failed to copy' }, 500);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
147
691
|
router.post('/*', async (c) => {
|
|
692
|
+
const config = getConfig();
|
|
148
693
|
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
149
|
-
const
|
|
694
|
+
const rootParam = c.req.query('root');
|
|
695
|
+
let dirPath;
|
|
696
|
+
if (rootParam) {
|
|
697
|
+
dirPath = validateRoot(rootParam, config);
|
|
698
|
+
if (!dirPath)
|
|
699
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
dirPath = findRootForPath(filePath, config);
|
|
703
|
+
if (!dirPath)
|
|
704
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
705
|
+
}
|
|
706
|
+
const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
707
|
+
const fullPath = path.join(dirPath, relativePath);
|
|
150
708
|
if (!isAllowed(fullPath, config)) {
|
|
151
709
|
return c.json({ error: 'Access denied' }, 403);
|
|
152
710
|
}
|
|
@@ -156,7 +714,19 @@ export function createFileRouter(config) {
|
|
|
156
714
|
if (body.type === 'create') {
|
|
157
715
|
const parentPath = body.parentPath || '';
|
|
158
716
|
const name = body.name;
|
|
159
|
-
|
|
717
|
+
let parentDirPath;
|
|
718
|
+
if (body.root) {
|
|
719
|
+
parentDirPath = validateRoot(body.root, config);
|
|
720
|
+
if (!parentDirPath) {
|
|
721
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
parentDirPath = findRootForPath(parentPath, config);
|
|
726
|
+
if (!parentDirPath)
|
|
727
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
728
|
+
}
|
|
729
|
+
const targetPath = path.join(parentDirPath, parentPath, name);
|
|
160
730
|
if (!isAllowed(targetPath, config)) {
|
|
161
731
|
return c.json({ error: 'Access denied' }, 403);
|
|
162
732
|
}
|
|
@@ -170,7 +740,7 @@ export function createFileRouter(config) {
|
|
|
170
740
|
}
|
|
171
741
|
await fs.writeFile(targetPath, '', 'utf-8');
|
|
172
742
|
}
|
|
173
|
-
return c.json({ success: true, path: targetPath.replace(
|
|
743
|
+
return c.json({ success: true, path: '/' + path.relative(parentDirPath, targetPath).replace(/\\/g, '/') });
|
|
174
744
|
}
|
|
175
745
|
catch (e) {
|
|
176
746
|
return c.json({ error: 'Failed to create' }, 500);
|
|
@@ -188,19 +758,24 @@ export function createFileRouter(config) {
|
|
|
188
758
|
}
|
|
189
759
|
});
|
|
190
760
|
router.put('/*', async (c) => {
|
|
191
|
-
const
|
|
192
|
-
const fullPath = path.join(config.root, filePath);
|
|
193
|
-
if (!isAllowed(fullPath, config)) {
|
|
194
|
-
return c.json({ error: 'Access denied' }, 403);
|
|
195
|
-
}
|
|
761
|
+
const config = getConfig();
|
|
196
762
|
try {
|
|
197
763
|
const body = await c.req.json();
|
|
198
|
-
const { oldPath, newPath, isDirectory } = body;
|
|
764
|
+
const { oldPath, newPath, isDirectory, sourceRoot, targetRoot } = body;
|
|
199
765
|
if (!oldPath || !newPath) {
|
|
200
766
|
return c.json({ error: 'oldPath and newPath are required' }, 400);
|
|
201
767
|
}
|
|
202
|
-
|
|
203
|
-
|
|
768
|
+
if (!sourceRoot || !targetRoot) {
|
|
769
|
+
return c.json({ error: 'sourceRoot and targetRoot are required' }, 400);
|
|
770
|
+
}
|
|
771
|
+
const validatedSourceRoot = validateRoot(sourceRoot, config);
|
|
772
|
+
const validatedTargetRoot = validateRoot(targetRoot, config);
|
|
773
|
+
if (!validatedSourceRoot)
|
|
774
|
+
return c.json({ error: 'Invalid source root' }, 400);
|
|
775
|
+
if (!validatedTargetRoot)
|
|
776
|
+
return c.json({ error: 'Invalid target root' }, 400);
|
|
777
|
+
const oldFullPath = path.join(validatedSourceRoot, oldPath);
|
|
778
|
+
const newFullPath = path.join(validatedTargetRoot, newPath);
|
|
204
779
|
if (!isAllowed(oldFullPath, config) || !isAllowed(newFullPath, config)) {
|
|
205
780
|
return c.json({ error: 'Access denied' }, 403);
|
|
206
781
|
}
|
|
@@ -209,15 +784,32 @@ export function createFileRouter(config) {
|
|
|
209
784
|
if (!stat.isDirectory()) {
|
|
210
785
|
return c.json({ error: 'Source is not a directory' }, 400);
|
|
211
786
|
}
|
|
212
|
-
await fs.rename(oldFullPath, newFullPath);
|
|
213
787
|
}
|
|
214
788
|
else {
|
|
215
789
|
const ext = path.extname(newPath).toLowerCase();
|
|
216
790
|
if (!hasAllowedExtension(newPath, config.allowedExtensions)) {
|
|
217
791
|
return c.json({ error: 'File type not allowed' }, 400);
|
|
218
792
|
}
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
219
795
|
await fs.rename(oldFullPath, newFullPath);
|
|
220
796
|
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
if (err instanceof Error && 'code' in err && err.code === 'EXDEV') {
|
|
799
|
+
// Cross-filesystem move - copy then delete
|
|
800
|
+
if (isDirectory) {
|
|
801
|
+
await fs.cp(oldFullPath, newFullPath, { recursive: true });
|
|
802
|
+
await fs.rm(oldFullPath, { recursive: true });
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
await fs.copyFile(oldFullPath, newFullPath);
|
|
806
|
+
await fs.unlink(oldFullPath);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
221
813
|
return c.json({ success: true, newPath });
|
|
222
814
|
}
|
|
223
815
|
catch (e) {
|
|
@@ -226,8 +818,22 @@ export function createFileRouter(config) {
|
|
|
226
818
|
}
|
|
227
819
|
});
|
|
228
820
|
router.delete('/*', async (c) => {
|
|
821
|
+
const config = getConfig();
|
|
229
822
|
const filePath = c.req.path.replace(/^\/api\/files/, '') || '/';
|
|
230
|
-
const
|
|
823
|
+
const rootParam = c.req.query('root');
|
|
824
|
+
let dirPath;
|
|
825
|
+
if (rootParam) {
|
|
826
|
+
dirPath = validateRoot(rootParam, config);
|
|
827
|
+
if (!dirPath)
|
|
828
|
+
return c.json({ error: 'Invalid root' }, 400);
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
dirPath = findRootForPath(filePath, config);
|
|
832
|
+
if (!dirPath)
|
|
833
|
+
return c.json({ error: 'Access denied' }, 403);
|
|
834
|
+
}
|
|
835
|
+
const relativePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
|
|
836
|
+
const fullPath = path.join(dirPath, relativePath);
|
|
231
837
|
if (!isAllowed(fullPath, config)) {
|
|
232
838
|
return c.json({ error: 'Access denied' }, 403);
|
|
233
839
|
}
|