@youtyan/code-viewer 0.2.5 → 0.2.6
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/dist/code-viewer.js +404 -95
- package/package.json +1 -1
- package/web/app.js +2867 -2820
package/dist/code-viewer.js
CHANGED
|
@@ -1458,6 +1458,9 @@ var init_git = __esm(() => {
|
|
|
1458
1458
|
"vendor",
|
|
1459
1459
|
".cache",
|
|
1460
1460
|
"coverage",
|
|
1461
|
+
"tmp",
|
|
1462
|
+
"log",
|
|
1463
|
+
"storage",
|
|
1461
1464
|
"DerivedData",
|
|
1462
1465
|
"Pods",
|
|
1463
1466
|
"bin",
|
|
@@ -3681,14 +3684,23 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3681
3684
|
const setTimer = options.setTimeoutFn || setTimeout;
|
|
3682
3685
|
const clearTimer = options.clearTimeoutFn || clearTimeout;
|
|
3683
3686
|
const debounceMs = options.debounceMs ?? 250;
|
|
3687
|
+
const maxWatchedDirectories = Math.max(1, Math.floor(options.maxWatchedDirectories ?? DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT));
|
|
3684
3688
|
const watchers = new Map;
|
|
3685
3689
|
const signatures = new Map;
|
|
3686
3690
|
const initialScanAsync = options.initialScanMode === "async" || (!options.watch || options.watch === nodeWatch) && !options.readdirSync;
|
|
3687
3691
|
const initialScanQueue = [];
|
|
3688
3692
|
let initialScanTimer = null;
|
|
3693
|
+
const pendingPathInspections = new Map;
|
|
3694
|
+
let pathInspectionTimer = null;
|
|
3689
3695
|
let timer = null;
|
|
3690
3696
|
const pendingChangedPaths = new Set;
|
|
3697
|
+
let watchLimitReported = false;
|
|
3691
3698
|
const ignored = (path) => isSkippableSearchPath(normalizeRelativePath(path), options.omitDirNames, options.excludeNames);
|
|
3699
|
+
const directoryRelativePath = (dir) => normalizeRelativePath(relative(options.root, dir));
|
|
3700
|
+
const ignoredDirectory = (dir) => {
|
|
3701
|
+
const rel = directoryRelativePath(dir);
|
|
3702
|
+
return Boolean(rel && ignored(rel));
|
|
3703
|
+
};
|
|
3692
3704
|
const scheduleUpdate = (changedPath) => {
|
|
3693
3705
|
if (changedPath)
|
|
3694
3706
|
pendingChangedPaths.add(changedPath);
|
|
@@ -3701,6 +3713,12 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3701
3713
|
options.onUpdate(paths);
|
|
3702
3714
|
}, debounceMs);
|
|
3703
3715
|
};
|
|
3716
|
+
const reportWatchLimit = () => {
|
|
3717
|
+
if (watchLimitReported)
|
|
3718
|
+
return;
|
|
3719
|
+
watchLimitReported = true;
|
|
3720
|
+
options.onError?.(new Error(`worktree watcher cap reached (${maxWatchedDirectories}); subsequent changes may be missed`));
|
|
3721
|
+
};
|
|
3704
3722
|
const closeSubtree = (dir) => {
|
|
3705
3723
|
for (const [watchedDir, watcher] of [...watchers]) {
|
|
3706
3724
|
if (watchedDir !== dir && !watchedDir.startsWith(`${dir}/`))
|
|
@@ -3717,7 +3735,12 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3717
3735
|
clearTimer(initialScanTimer);
|
|
3718
3736
|
initialScanTimer = null;
|
|
3719
3737
|
}
|
|
3738
|
+
if (pathInspectionTimer) {
|
|
3739
|
+
clearTimer(pathInspectionTimer);
|
|
3740
|
+
pathInspectionTimer = null;
|
|
3741
|
+
}
|
|
3720
3742
|
initialScanQueue.length = 0;
|
|
3743
|
+
pendingPathInspections.clear();
|
|
3721
3744
|
for (const watcher of [...watchers.values()]) {
|
|
3722
3745
|
try {
|
|
3723
3746
|
watcher.close?.();
|
|
@@ -3738,27 +3761,82 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3738
3761
|
for (const entry of entries) {
|
|
3739
3762
|
if (!entry.isDirectory())
|
|
3740
3763
|
continue;
|
|
3741
|
-
|
|
3764
|
+
const child = join7(dir, entry.name);
|
|
3765
|
+
if (ignoredDirectory(child))
|
|
3766
|
+
continue;
|
|
3767
|
+
children.push(child);
|
|
3742
3768
|
}
|
|
3743
3769
|
return children;
|
|
3744
3770
|
};
|
|
3745
3771
|
const processInitialScanQueue = () => {
|
|
3746
3772
|
initialScanTimer = null;
|
|
3773
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3774
|
+
reportWatchLimit();
|
|
3775
|
+
initialScanQueue.length = 0;
|
|
3776
|
+
return;
|
|
3777
|
+
}
|
|
3747
3778
|
const next = initialScanQueue.shift();
|
|
3748
3779
|
if (next)
|
|
3749
3780
|
watchDirectory(next, true);
|
|
3781
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3782
|
+
reportWatchLimit();
|
|
3783
|
+
initialScanQueue.length = 0;
|
|
3784
|
+
}
|
|
3750
3785
|
if (initialScanQueue.length)
|
|
3751
3786
|
initialScanTimer = setTimer(processInitialScanQueue, 50);
|
|
3752
3787
|
};
|
|
3753
3788
|
const queueInitialChildren = (dir) => {
|
|
3754
|
-
|
|
3789
|
+
const remaining = maxWatchedDirectories - watchers.size;
|
|
3790
|
+
if (remaining <= 0) {
|
|
3791
|
+
reportWatchLimit();
|
|
3792
|
+
return;
|
|
3793
|
+
}
|
|
3794
|
+
const children = readChildDirectories(dir);
|
|
3795
|
+
if (children.length > remaining)
|
|
3796
|
+
reportWatchLimit();
|
|
3797
|
+
initialScanQueue.push(...children.slice(0, remaining));
|
|
3755
3798
|
if (!initialScanTimer)
|
|
3756
3799
|
initialScanTimer = setTimer(processInitialScanQueue, 5000);
|
|
3757
3800
|
};
|
|
3801
|
+
const processChangedPath = (changed, fullChangedPath) => {
|
|
3802
|
+
const known = watchers.has(fullChangedPath);
|
|
3803
|
+
if (isDirectory(fullChangedPath)) {
|
|
3804
|
+
if (known) {
|
|
3805
|
+
const signature = directorySignature(fullChangedPath);
|
|
3806
|
+
if (signature && signature !== signatures.get(fullChangedPath)) {
|
|
3807
|
+
closeSubtree(fullChangedPath);
|
|
3808
|
+
watchDirectory(fullChangedPath, initialScanAsync);
|
|
3809
|
+
}
|
|
3810
|
+
scheduleUpdate(changed);
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
watchDirectory(fullChangedPath, initialScanAsync);
|
|
3814
|
+
} else if (known) {
|
|
3815
|
+
closeSubtree(fullChangedPath);
|
|
3816
|
+
}
|
|
3817
|
+
scheduleUpdate(changed);
|
|
3818
|
+
};
|
|
3819
|
+
const processPathInspections = () => {
|
|
3820
|
+
pathInspectionTimer = null;
|
|
3821
|
+
const entries = [...pendingPathInspections];
|
|
3822
|
+
pendingPathInspections.clear();
|
|
3823
|
+
for (const [changed, fullChangedPath] of entries) {
|
|
3824
|
+
processChangedPath(changed, fullChangedPath);
|
|
3825
|
+
}
|
|
3826
|
+
};
|
|
3827
|
+
const queuePathInspection = (changed, fullChangedPath) => {
|
|
3828
|
+
pendingPathInspections.set(changed, fullChangedPath);
|
|
3829
|
+
if (!pathInspectionTimer)
|
|
3830
|
+
pathInspectionTimer = setTimer(processPathInspections, 25);
|
|
3831
|
+
};
|
|
3758
3832
|
const watchDirectory = (dir, initialScan = false) => {
|
|
3759
3833
|
if (watchers.has(dir))
|
|
3760
3834
|
return;
|
|
3761
|
-
|
|
3835
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3836
|
+
reportWatchLimit();
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
const rel = directoryRelativePath(dir);
|
|
3762
3840
|
if (rel && ignored(rel))
|
|
3763
3841
|
return;
|
|
3764
3842
|
try {
|
|
@@ -3773,22 +3851,11 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3773
3851
|
const fullChangedPath = join7(options.root, changed);
|
|
3774
3852
|
if (!isInsideRoot(options.root, fullChangedPath))
|
|
3775
3853
|
return;
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
const signature2 = directorySignature(fullChangedPath);
|
|
3780
|
-
if (signature2 && signature2 !== signatures.get(fullChangedPath)) {
|
|
3781
|
-
closeSubtree(fullChangedPath);
|
|
3782
|
-
watchDirectory(fullChangedPath);
|
|
3783
|
-
}
|
|
3784
|
-
scheduleUpdate(changed);
|
|
3785
|
-
return;
|
|
3786
|
-
}
|
|
3787
|
-
watchDirectory(fullChangedPath);
|
|
3788
|
-
} else if (known) {
|
|
3789
|
-
closeSubtree(fullChangedPath);
|
|
3854
|
+
if (initialScanAsync) {
|
|
3855
|
+
queuePathInspection(changed, fullChangedPath);
|
|
3856
|
+
return;
|
|
3790
3857
|
}
|
|
3791
|
-
|
|
3858
|
+
processChangedPath(changed, fullChangedPath);
|
|
3792
3859
|
}) || {};
|
|
3793
3860
|
watchers.set(dir, watcher);
|
|
3794
3861
|
const signature = directorySignature(dir);
|
|
@@ -3814,12 +3881,17 @@ function startWorktreeUpdateWatch(options) {
|
|
|
3814
3881
|
queueInitialChildren(dir);
|
|
3815
3882
|
return;
|
|
3816
3883
|
}
|
|
3884
|
+
if (watchers.size >= maxWatchedDirectories) {
|
|
3885
|
+
reportWatchLimit();
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3817
3888
|
for (const child of readChildDirectories(dir))
|
|
3818
3889
|
watchDirectory(child);
|
|
3819
3890
|
};
|
|
3820
3891
|
watchDirectory(options.root, true);
|
|
3821
3892
|
return { started: watchers.size > 0, close: closeAll };
|
|
3822
3893
|
}
|
|
3894
|
+
var DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT = 256;
|
|
3823
3895
|
var init_worktree_watcher = __esm(() => {
|
|
3824
3896
|
init_search();
|
|
3825
3897
|
});
|
|
@@ -5855,7 +5927,7 @@ function parseComposeEnv(serviceBlock, composeDirEnv = {}) {
|
|
|
5855
5927
|
}
|
|
5856
5928
|
return env;
|
|
5857
5929
|
}
|
|
5858
|
-
function parseComposePortMappings(serviceBlock) {
|
|
5930
|
+
function parseComposePortMappings(serviceBlock, composeDirEnv = {}) {
|
|
5859
5931
|
const portsMatch = serviceBlock.match(/^[ \t]+ports:\s*\n((?:[ \t]+- [^\n]+\n?)*)/m);
|
|
5860
5932
|
if (!portsMatch)
|
|
5861
5933
|
return [];
|
|
@@ -5865,26 +5937,45 @@ function parseComposePortMappings(serviceBlock) {
|
|
|
5865
5937
|
const trimmed = line.trim();
|
|
5866
5938
|
if (!trimmed.startsWith("-"))
|
|
5867
5939
|
continue;
|
|
5868
|
-
const value = trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim();
|
|
5940
|
+
const value = resolveEnvValue(trimmed.slice(1).trim().replace(/^["']|["']$/g, "").split(/\s+#/)[0].split("/")[0].trim(), composeDirEnv);
|
|
5869
5941
|
if (!value || value.includes("target:"))
|
|
5870
5942
|
continue;
|
|
5871
5943
|
const parts = value.split(":");
|
|
5872
5944
|
const container = parts.pop()?.trim() || "";
|
|
5873
5945
|
const host = parts.pop()?.trim() || "";
|
|
5874
|
-
|
|
5946
|
+
const containerPorts = expandPortRange(container);
|
|
5947
|
+
const hostPorts = host ? expandPortRange(host) : [];
|
|
5948
|
+
if (!containerPorts)
|
|
5875
5949
|
continue;
|
|
5876
|
-
if (host &&
|
|
5950
|
+
if (host && (!hostPorts || hostPorts.length !== containerPorts.length)) {
|
|
5877
5951
|
continue;
|
|
5878
|
-
|
|
5952
|
+
}
|
|
5953
|
+
for (let i = 0;i < containerPorts.length; i++) {
|
|
5954
|
+
mappings.push({
|
|
5955
|
+
host: hostPorts ? (hostPorts[i] ?? "").toString() : "",
|
|
5956
|
+
container: containerPorts[i].toString()
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5879
5959
|
}
|
|
5880
5960
|
return mappings;
|
|
5881
5961
|
}
|
|
5882
|
-
function
|
|
5883
|
-
const
|
|
5962
|
+
function expandPortRange(value) {
|
|
5963
|
+
const match = value.match(/^(\d+)(?:-(\d+))?$/);
|
|
5964
|
+
if (!match)
|
|
5965
|
+
return null;
|
|
5966
|
+
const start = Number(match[1]);
|
|
5967
|
+
const end = Number(match[2] || match[1]);
|
|
5968
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 1 || end > 65535 || end < start) {
|
|
5969
|
+
return null;
|
|
5970
|
+
}
|
|
5971
|
+
return Array.from({ length: end - start + 1 }, (_, idx) => start + idx);
|
|
5972
|
+
}
|
|
5973
|
+
function parseComposePorts(serviceBlock, composeDirEnv = {}) {
|
|
5974
|
+
const first = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.host);
|
|
5884
5975
|
return first?.host || null;
|
|
5885
5976
|
}
|
|
5886
|
-
function parseComposeHostPortForContainer(serviceBlock, containerPort) {
|
|
5887
|
-
const found = parseComposePortMappings(serviceBlock).find((m) => m.container === containerPort && m.host);
|
|
5977
|
+
function parseComposeHostPortForContainer(serviceBlock, containerPort, composeDirEnv = {}) {
|
|
5978
|
+
const found = parseComposePortMappings(serviceBlock, composeDirEnv).find((m) => m.container === containerPort && m.host);
|
|
5888
5979
|
return found?.host || null;
|
|
5889
5980
|
}
|
|
5890
5981
|
function parseComposeContainerPort(serviceBlock) {
|
|
@@ -5935,7 +6026,7 @@ function parseComposeContent(content, filepath, composeDir, cwd, composeDirEnv,
|
|
|
5935
6026
|
continue;
|
|
5936
6027
|
const defaultPort = defaultPortFor(kind, image, env);
|
|
5937
6028
|
const serviceContainerPort = kind === "s3" ? defaultPort : containerPort || defaultPort;
|
|
5938
|
-
const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort) || parseComposeHostPortForContainer(svcBlock, defaultPort) || parseComposePorts(svcBlock);
|
|
6029
|
+
const publishedHostPort = parseComposeHostPortForContainer(svcBlock, serviceContainerPort, composeDirEnv) || parseComposeHostPortForContainer(svcBlock, defaultPort, composeDirEnv) || parseComposePorts(svcBlock, composeDirEnv);
|
|
5939
6030
|
const hostPort = kind === "s3" ? publishedHostPort || undefined : publishedHostPort || defaultPort;
|
|
5940
6031
|
const imageLabel = image ?? `build:${kind}`;
|
|
5941
6032
|
const id = isRoot ? `docker:${svc.name}` : `docker:${svc.name}@${encodeURIComponent(relDirSlash)}`;
|
|
@@ -6672,6 +6763,37 @@ function textError(message, status) {
|
|
|
6672
6763
|
}
|
|
6673
6764
|
});
|
|
6674
6765
|
}
|
|
6766
|
+
function waitForCallerAbort(promise, signal, message) {
|
|
6767
|
+
if (!signal)
|
|
6768
|
+
return promise;
|
|
6769
|
+
if (signal.aborted)
|
|
6770
|
+
return Promise.reject(abortError(message));
|
|
6771
|
+
return new Promise((resolve2, reject) => {
|
|
6772
|
+
let settled = false;
|
|
6773
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
6774
|
+
const onAbort = () => {
|
|
6775
|
+
if (settled)
|
|
6776
|
+
return;
|
|
6777
|
+
settled = true;
|
|
6778
|
+
cleanup();
|
|
6779
|
+
reject(abortError(message));
|
|
6780
|
+
};
|
|
6781
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6782
|
+
promise.then((value) => {
|
|
6783
|
+
if (settled)
|
|
6784
|
+
return;
|
|
6785
|
+
settled = true;
|
|
6786
|
+
cleanup();
|
|
6787
|
+
resolve2(value);
|
|
6788
|
+
}, (err) => {
|
|
6789
|
+
if (settled)
|
|
6790
|
+
return;
|
|
6791
|
+
settled = true;
|
|
6792
|
+
cleanup();
|
|
6793
|
+
reject(err);
|
|
6794
|
+
});
|
|
6795
|
+
});
|
|
6796
|
+
}
|
|
6675
6797
|
async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omitDirNames, signal) {
|
|
6676
6798
|
if (!dbParam)
|
|
6677
6799
|
return textError("missing db parameter", 400);
|
|
@@ -6681,10 +6803,26 @@ async function resolveDockerExplorerAsync(cwd, dbParam, kind, cache, openFn, omi
|
|
|
6681
6803
|
const parsed = parseDockerDbId(dbParam);
|
|
6682
6804
|
if (!parsed)
|
|
6683
6805
|
return textError("invalid docker db id", 400);
|
|
6684
|
-
|
|
6806
|
+
let info;
|
|
6807
|
+
try {
|
|
6808
|
+
info = await findDockerServiceByDbIdAsync(cwd, dbParam, kind, omitDirNames, signal);
|
|
6809
|
+
} catch (err) {
|
|
6810
|
+
if (isAbortLikeError(err, signal)) {
|
|
6811
|
+
return textError(`${kind} lookup aborted`, 503);
|
|
6812
|
+
}
|
|
6813
|
+
throw err;
|
|
6814
|
+
}
|
|
6685
6815
|
if (!info)
|
|
6686
6816
|
return textError(`${kind} service not found`, 404);
|
|
6687
|
-
|
|
6817
|
+
let explorer;
|
|
6818
|
+
try {
|
|
6819
|
+
explorer = await waitForCallerAbort(cache.getOrOpenAsync(dbParam, () => openFn(info)), signal, `${kind} open aborted`);
|
|
6820
|
+
} catch (err) {
|
|
6821
|
+
if (isAbortLikeError(err, signal)) {
|
|
6822
|
+
return textError(`${kind} open aborted`, 503);
|
|
6823
|
+
}
|
|
6824
|
+
throw err;
|
|
6825
|
+
}
|
|
6688
6826
|
return { dbId: dbParam, explorer };
|
|
6689
6827
|
}
|
|
6690
6828
|
async function dispatchRoutes(req, url, routes, sideEffectAllowed, wrap = (res) => res, handleRouteError) {
|
|
@@ -7591,6 +7729,31 @@ function s3ObjectName(key) {
|
|
|
7591
7729
|
// web-src/server/database/adapters/s3.ts
|
|
7592
7730
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
7593
7731
|
import { createHash as createHash4, createHmac } from "node:crypto";
|
|
7732
|
+
function createS3RequestDeadline() {
|
|
7733
|
+
const timeoutMs = s3RequestTimeoutMs;
|
|
7734
|
+
return {
|
|
7735
|
+
expiresAt: Date.now() + timeoutMs,
|
|
7736
|
+
timeoutMs
|
|
7737
|
+
};
|
|
7738
|
+
}
|
|
7739
|
+
function createS3DockerCurlDeadline() {
|
|
7740
|
+
const timeoutMs = s3DockerCurlTimeoutMs;
|
|
7741
|
+
return {
|
|
7742
|
+
expiresAt: Date.now() + timeoutMs,
|
|
7743
|
+
timeoutMs
|
|
7744
|
+
};
|
|
7745
|
+
}
|
|
7746
|
+
function createS3TransportDeadline(config) {
|
|
7747
|
+
return config.dockerContainerName ? createS3DockerCurlDeadline() : createS3RequestDeadline();
|
|
7748
|
+
}
|
|
7749
|
+
function s3TimeoutError(deadline) {
|
|
7750
|
+
return new S3HttpError(503, `S3 request timed out after ${deadline?.timeoutMs ?? s3RequestTimeoutMs}ms`);
|
|
7751
|
+
}
|
|
7752
|
+
function remainingS3TimeoutMs(deadline) {
|
|
7753
|
+
if (!deadline)
|
|
7754
|
+
return s3RequestTimeoutMs;
|
|
7755
|
+
return Math.max(0, deadline.expiresAt - Date.now());
|
|
7756
|
+
}
|
|
7594
7757
|
function hmac(key, value) {
|
|
7595
7758
|
return createHmac("sha256", key).update(value, "utf8").digest();
|
|
7596
7759
|
}
|
|
@@ -7725,13 +7888,139 @@ function dockerCurlCommand(opts) {
|
|
|
7725
7888
|
input: Buffer.from(curlHeaderConfig(opts.headers), "utf8")
|
|
7726
7889
|
};
|
|
7727
7890
|
}
|
|
7891
|
+
function guardedS3Transport(signal, operation, deadline) {
|
|
7892
|
+
if (signal?.aborted) {
|
|
7893
|
+
return Promise.reject(new S3HttpError(503, "S3 HTTP transport aborted"));
|
|
7894
|
+
}
|
|
7895
|
+
const timeoutMs = remainingS3TimeoutMs(deadline);
|
|
7896
|
+
if (timeoutMs <= 0) {
|
|
7897
|
+
return Promise.reject(s3TimeoutError(deadline));
|
|
7898
|
+
}
|
|
7899
|
+
const controller = new AbortController;
|
|
7900
|
+
let timedOut = false;
|
|
7901
|
+
let settled = false;
|
|
7902
|
+
let timer;
|
|
7903
|
+
let cleanupParent = () => {};
|
|
7904
|
+
const abort = (err, reject) => {
|
|
7905
|
+
if (settled)
|
|
7906
|
+
return;
|
|
7907
|
+
settled = true;
|
|
7908
|
+
controller.abort(err);
|
|
7909
|
+
reject(err);
|
|
7910
|
+
};
|
|
7911
|
+
const guarded = new Promise((resolve2, reject) => {
|
|
7912
|
+
const onParentAbort = () => abort(new S3HttpError(503, "S3 HTTP transport aborted"), reject);
|
|
7913
|
+
if (signal) {
|
|
7914
|
+
signal.addEventListener("abort", onParentAbort, { once: true });
|
|
7915
|
+
cleanupParent = () => signal.removeEventListener("abort", onParentAbort);
|
|
7916
|
+
}
|
|
7917
|
+
timer = setTimeout(() => {
|
|
7918
|
+
timedOut = true;
|
|
7919
|
+
abort(s3TimeoutError(deadline), reject);
|
|
7920
|
+
}, timeoutMs);
|
|
7921
|
+
operation(controller.signal).then((value) => {
|
|
7922
|
+
if (settled)
|
|
7923
|
+
return;
|
|
7924
|
+
settled = true;
|
|
7925
|
+
resolve2(value);
|
|
7926
|
+
}, (err) => {
|
|
7927
|
+
if (settled)
|
|
7928
|
+
return;
|
|
7929
|
+
settled = true;
|
|
7930
|
+
if (timedOut) {
|
|
7931
|
+
reject(s3TimeoutError(deadline));
|
|
7932
|
+
} else if (signal?.aborted) {
|
|
7933
|
+
reject(new S3HttpError(503, "S3 HTTP transport aborted"));
|
|
7934
|
+
} else {
|
|
7935
|
+
reject(err);
|
|
7936
|
+
}
|
|
7937
|
+
});
|
|
7938
|
+
});
|
|
7939
|
+
return guarded.finally(() => {
|
|
7940
|
+
if (timer)
|
|
7941
|
+
clearTimeout(timer);
|
|
7942
|
+
cleanupParent();
|
|
7943
|
+
});
|
|
7944
|
+
}
|
|
7945
|
+
async function readStreamChunkWithTimeout(reader, signal, deadline) {
|
|
7946
|
+
return guardedS3Transport(signal, (transportSignal) => {
|
|
7947
|
+
const cancelRead = () => {
|
|
7948
|
+
reader.cancel(transportSignal.reason).catch(() => {});
|
|
7949
|
+
};
|
|
7950
|
+
if (transportSignal.aborted) {
|
|
7951
|
+
cancelRead();
|
|
7952
|
+
} else {
|
|
7953
|
+
transportSignal.addEventListener("abort", cancelRead, { once: true });
|
|
7954
|
+
}
|
|
7955
|
+
return reader.read().finally(() => {
|
|
7956
|
+
transportSignal.removeEventListener("abort", cancelRead);
|
|
7957
|
+
});
|
|
7958
|
+
}, deadline);
|
|
7959
|
+
}
|
|
7960
|
+
async function readResponseBytesWithTimeout(res, signal, deadline) {
|
|
7961
|
+
if (!res.body)
|
|
7962
|
+
return new Uint8Array;
|
|
7963
|
+
const reader = res.body.getReader();
|
|
7964
|
+
const chunks = [];
|
|
7965
|
+
let total = 0;
|
|
7966
|
+
try {
|
|
7967
|
+
for (;; ) {
|
|
7968
|
+
const { done, value } = await readStreamChunkWithTimeout(reader, signal, deadline);
|
|
7969
|
+
if (done)
|
|
7970
|
+
break;
|
|
7971
|
+
if (!value?.byteLength)
|
|
7972
|
+
continue;
|
|
7973
|
+
chunks.push(value);
|
|
7974
|
+
total += value.byteLength;
|
|
7975
|
+
}
|
|
7976
|
+
} finally {
|
|
7977
|
+
try {
|
|
7978
|
+
reader.releaseLock();
|
|
7979
|
+
} catch {}
|
|
7980
|
+
}
|
|
7981
|
+
if (chunks.length === 1)
|
|
7982
|
+
return chunks[0];
|
|
7983
|
+
const bytes = new Uint8Array(total);
|
|
7984
|
+
let offset = 0;
|
|
7985
|
+
for (const chunk of chunks) {
|
|
7986
|
+
bytes.set(chunk, offset);
|
|
7987
|
+
offset += chunk.byteLength;
|
|
7988
|
+
}
|
|
7989
|
+
return bytes;
|
|
7990
|
+
}
|
|
7991
|
+
async function readResponseTextWithTimeout(res, signal, deadline) {
|
|
7992
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(await readResponseBytesWithTimeout(res, signal, deadline));
|
|
7993
|
+
}
|
|
7994
|
+
function timeoutReadableStream(body, signal) {
|
|
7995
|
+
if (!body)
|
|
7996
|
+
return null;
|
|
7997
|
+
const reader = body.getReader();
|
|
7998
|
+
return new ReadableStream({
|
|
7999
|
+
async pull(controller) {
|
|
8000
|
+
try {
|
|
8001
|
+
const { done, value } = await readStreamChunkWithTimeout(reader, signal);
|
|
8002
|
+
if (done) {
|
|
8003
|
+
controller.close();
|
|
8004
|
+
return;
|
|
8005
|
+
}
|
|
8006
|
+
if (value)
|
|
8007
|
+
controller.enqueue(value);
|
|
8008
|
+
} catch (error) {
|
|
8009
|
+
controller.error(error);
|
|
8010
|
+
}
|
|
8011
|
+
},
|
|
8012
|
+
cancel(reason) {
|
|
8013
|
+
return reader.cancel(reason);
|
|
8014
|
+
}
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
7728
8017
|
async function dockerCurlFetch(opts) {
|
|
7729
8018
|
const { args, input } = dockerCurlCommand(opts);
|
|
7730
8019
|
if (spawnSyncImplIsTestOverride) {
|
|
7731
8020
|
const proc2 = spawnSyncImpl3("docker", args, {
|
|
7732
8021
|
encoding: "buffer",
|
|
7733
8022
|
input,
|
|
7734
|
-
timeout:
|
|
8023
|
+
timeout: s3DockerCurlTimeoutMs,
|
|
7735
8024
|
stdio: ["pipe", "pipe", "pipe"]
|
|
7736
8025
|
});
|
|
7737
8026
|
if ((proc2.status ?? 1) !== 0) {
|
|
@@ -7747,10 +8036,10 @@ async function dockerCurlFetch(opts) {
|
|
|
7747
8036
|
command: "docker",
|
|
7748
8037
|
args,
|
|
7749
8038
|
input,
|
|
7750
|
-
timeoutMs:
|
|
8039
|
+
timeoutMs: s3DockerCurlTimeoutMs,
|
|
7751
8040
|
signal: opts.signal,
|
|
7752
8041
|
abortMessage: "S3 HTTP transport aborted",
|
|
7753
|
-
timeoutMessage:
|
|
8042
|
+
timeoutMessage: `docker exec curl timed out after ${s3DockerCurlTimeoutMs}ms`
|
|
7754
8043
|
});
|
|
7755
8044
|
if (proc.code !== 0) {
|
|
7756
8045
|
const stderr = new TextDecoder().decode(proc.stderr).replace(/\s+/g, " ").trim();
|
|
@@ -7824,73 +8113,77 @@ function parseObjects(xml) {
|
|
|
7824
8113
|
};
|
|
7825
8114
|
}
|
|
7826
8115
|
function createS3Adapter(config) {
|
|
7827
|
-
async function signedFetch(opts) {
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
8116
|
+
async function signedFetch(opts, deadline = createS3TransportDeadline(config)) {
|
|
8117
|
+
return guardedS3Transport(opts.signal, (transportSignal) => {
|
|
8118
|
+
const endpoint = new URL(config.endpoint);
|
|
8119
|
+
const { dateStamp, amzDate: requestDate } = amzDate();
|
|
8120
|
+
const path = buildPath(opts.bucket, opts.key);
|
|
8121
|
+
const query = canonicalQuery(opts.query);
|
|
8122
|
+
const url = `${config.endpoint.replace(/\/$/, "")}${path}${query ? `?${query}` : ""}`;
|
|
8123
|
+
const headers = {
|
|
8124
|
+
host: endpoint.host,
|
|
8125
|
+
"x-amz-content-sha256": EMPTY_SHA256,
|
|
8126
|
+
"x-amz-date": requestDate,
|
|
8127
|
+
...config.sessionToken ? { "x-amz-security-token": config.sessionToken } : {},
|
|
8128
|
+
...opts.headers || {}
|
|
8129
|
+
};
|
|
8130
|
+
const signedNames = signedHeadersString(headers);
|
|
8131
|
+
const canonicalRequest = [
|
|
8132
|
+
opts.method,
|
|
8133
|
+
path,
|
|
8134
|
+
query,
|
|
8135
|
+
canonicalHeaders(headers),
|
|
8136
|
+
signedNames,
|
|
8137
|
+
EMPTY_SHA256
|
|
8138
|
+
].join(`
|
|
7849
8139
|
`);
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
8140
|
+
const scope = `${dateStamp}/${config.region}/s3/aws4_request`;
|
|
8141
|
+
const stringToSign = [
|
|
8142
|
+
"AWS4-HMAC-SHA256",
|
|
8143
|
+
requestDate,
|
|
8144
|
+
scope,
|
|
8145
|
+
sha256(canonicalRequest)
|
|
8146
|
+
].join(`
|
|
7857
8147
|
`);
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
}
|
|
7864
|
-
requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
|
|
7865
|
-
if (config.dockerContainerName) {
|
|
7866
|
-
if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
|
|
7867
|
-
throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
|
|
8148
|
+
const signature = createHmac("sha256", signingKey(config.secretAccessKey, dateStamp, config.region)).update(stringToSign, "utf8").digest("hex");
|
|
8149
|
+
const requestHeaders = new Headers;
|
|
8150
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
8151
|
+
if (key !== "host")
|
|
8152
|
+
requestHeaders.set(key, value);
|
|
7868
8153
|
}
|
|
7869
|
-
|
|
7870
|
-
|
|
8154
|
+
requestHeaders.set("Authorization", `AWS4-HMAC-SHA256 Credential=${config.accessKeyId}/${scope}, SignedHeaders=${signedNames}, Signature=${signature}`);
|
|
8155
|
+
if (config.dockerContainerName) {
|
|
8156
|
+
if (opts.method === "GET" && opts.key && !opts.headers?.range && !opts.headers?.Range) {
|
|
8157
|
+
throw new S3HttpError(503, "S3 raw streaming requires a published host port or a ranged request");
|
|
8158
|
+
}
|
|
8159
|
+
return dockerCurlFetch({
|
|
8160
|
+
containerName: config.dockerContainerName,
|
|
8161
|
+
method: opts.method,
|
|
8162
|
+
url,
|
|
8163
|
+
headers: requestHeaders,
|
|
8164
|
+
signal: transportSignal
|
|
8165
|
+
});
|
|
8166
|
+
}
|
|
8167
|
+
return fetch(url, {
|
|
7871
8168
|
method: opts.method,
|
|
7872
|
-
url,
|
|
7873
8169
|
headers: requestHeaders,
|
|
7874
|
-
signal:
|
|
8170
|
+
signal: transportSignal
|
|
7875
8171
|
});
|
|
7876
|
-
}
|
|
7877
|
-
return fetch(url, {
|
|
7878
|
-
method: opts.method,
|
|
7879
|
-
headers: requestHeaders,
|
|
7880
|
-
signal: opts.signal
|
|
7881
|
-
});
|
|
8172
|
+
}, deadline);
|
|
7882
8173
|
}
|
|
7883
|
-
async function textOrThrow(res) {
|
|
7884
|
-
const text = await res
|
|
8174
|
+
async function textOrThrow(res, signal, deadline) {
|
|
8175
|
+
const text = await readResponseTextWithTimeout(res, signal, deadline);
|
|
7885
8176
|
if (!res.ok)
|
|
7886
8177
|
throw sanitizeS3Error(res.status, text);
|
|
7887
8178
|
return text;
|
|
7888
8179
|
}
|
|
7889
8180
|
async function listBuckets(signal) {
|
|
7890
|
-
const
|
|
8181
|
+
const deadline = createS3TransportDeadline(config);
|
|
8182
|
+
const xml = await textOrThrow(await signedFetch({ method: "GET", signal }, deadline), signal, deadline);
|
|
7891
8183
|
return parseBuckets(xml);
|
|
7892
8184
|
}
|
|
7893
8185
|
async function listObjects(opts) {
|
|
8186
|
+
const deadline = createS3TransportDeadline(config);
|
|
7894
8187
|
const xml = await textOrThrow(await signedFetch({
|
|
7895
8188
|
method: "GET",
|
|
7896
8189
|
bucket: opts.bucket,
|
|
@@ -7901,32 +8194,34 @@ function createS3Adapter(config) {
|
|
|
7901
8194
|
...opts.continuationToken ? { "continuation-token": opts.continuationToken } : {}
|
|
7902
8195
|
},
|
|
7903
8196
|
signal: opts.signal
|
|
7904
|
-
}));
|
|
8197
|
+
}, deadline), opts.signal, deadline);
|
|
7905
8198
|
return parseObjects(xml);
|
|
7906
8199
|
}
|
|
7907
8200
|
async function headObject(opts) {
|
|
8201
|
+
const deadline = createS3TransportDeadline(config);
|
|
7908
8202
|
const res = await signedFetch({
|
|
7909
8203
|
method: "HEAD",
|
|
7910
8204
|
bucket: opts.bucket,
|
|
7911
8205
|
key: opts.key,
|
|
7912
8206
|
signal: opts.signal
|
|
7913
|
-
});
|
|
8207
|
+
}, deadline);
|
|
7914
8208
|
if (!res.ok)
|
|
7915
|
-
throw sanitizeS3Error(res.status, await res.
|
|
8209
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
7916
8210
|
return headFromObjectResponse(opts.bucket, opts.key, res);
|
|
7917
8211
|
}
|
|
7918
8212
|
async function getObjectText(opts) {
|
|
7919
8213
|
const maxBytes = Math.min(1024 * 1024, Math.max(1, opts.maxBytes ?? 512 * 1024));
|
|
8214
|
+
const deadline = createS3TransportDeadline(config);
|
|
7920
8215
|
const res = await signedFetch({
|
|
7921
8216
|
method: "GET",
|
|
7922
8217
|
bucket: opts.bucket,
|
|
7923
8218
|
key: opts.key,
|
|
7924
8219
|
headers: { range: `bytes=0-${maxBytes - 1}` },
|
|
7925
8220
|
signal: opts.signal
|
|
7926
|
-
});
|
|
8221
|
+
}, deadline);
|
|
7927
8222
|
if (!res.ok && res.status !== 206)
|
|
7928
|
-
throw sanitizeS3Error(res.status, await res.
|
|
7929
|
-
const bytes =
|
|
8223
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
8224
|
+
const bytes = await readResponseBytesWithTimeout(res, opts.signal, deadline);
|
|
7930
8225
|
const head = headFromObjectResponse(opts.bucket, opts.key, res);
|
|
7931
8226
|
const fullSize = head.sizeBytes;
|
|
7932
8227
|
return {
|
|
@@ -7936,17 +8231,18 @@ function createS3Adapter(config) {
|
|
|
7936
8231
|
};
|
|
7937
8232
|
}
|
|
7938
8233
|
async function getObjectResponse(opts) {
|
|
8234
|
+
const deadline = createS3TransportDeadline(config);
|
|
7939
8235
|
const res = await signedFetch({
|
|
7940
8236
|
method: opts.method,
|
|
7941
8237
|
bucket: opts.bucket,
|
|
7942
8238
|
key: opts.key,
|
|
7943
8239
|
headers: opts.range ? { range: opts.range } : undefined,
|
|
7944
8240
|
signal: opts.signal
|
|
7945
|
-
});
|
|
8241
|
+
}, deadline);
|
|
7946
8242
|
if (!res.ok && res.status !== 206) {
|
|
7947
|
-
throw sanitizeS3Error(res.status, await res.
|
|
8243
|
+
throw sanitizeS3Error(res.status, await readResponseTextWithTimeout(res, opts.signal, deadline));
|
|
7948
8244
|
}
|
|
7949
|
-
return new Response(opts.method === "HEAD" ? null : res.body, {
|
|
8245
|
+
return new Response(opts.method === "HEAD" ? null : timeoutReadableStream(res.body, opts.signal), {
|
|
7950
8246
|
status: res.status,
|
|
7951
8247
|
headers: rawObjectHeaders(opts.key, res)
|
|
7952
8248
|
});
|
|
@@ -7966,6 +8262,9 @@ async function s3ConfigFromDockerInfoAsync(info, signal) {
|
|
|
7966
8262
|
const image = info.image?.toLowerCase() || "";
|
|
7967
8263
|
const minioDefault = image.includes("minio");
|
|
7968
8264
|
const env = info.env;
|
|
8265
|
+
if (!info.hostPort && minioDefault) {
|
|
8266
|
+
throw new S3HttpError(503, 'MinIO S3 browsing requires a published host port. Add a compose port mapping like "9000:9000" for the MinIO API.');
|
|
8267
|
+
}
|
|
7969
8268
|
const dockerContainerName = info.hostPort ? undefined : await resolveRunningComposeContainerNameOrThrowAsync(info.serviceName, info.composeDir, signal);
|
|
7970
8269
|
return {
|
|
7971
8270
|
endpoint: info.hostPort ? `http://localhost:${info.hostPort}` : `http://127.0.0.1:${info.containerPort}`,
|
|
@@ -7982,7 +8281,7 @@ async function openS3ExplorerAsync(info, signal) {
|
|
|
7982
8281
|
function isS3HttpError(err) {
|
|
7983
8282
|
return err instanceof S3HttpError;
|
|
7984
8283
|
}
|
|
7985
|
-
var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
8284
|
+
var spawnSyncImpl3, spawnSyncImplIsTestOverride = false, S3HttpError, EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", DEFAULT_S3_REQUEST_TIMEOUT_MS = 5000, DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS = 30000, s3RequestTimeoutMs, s3DockerCurlTimeoutMs;
|
|
7986
8285
|
var init_s3 = __esm(() => {
|
|
7987
8286
|
init_raw_file_headers();
|
|
7988
8287
|
init_docker_utils();
|
|
@@ -7995,6 +8294,8 @@ var init_s3 = __esm(() => {
|
|
|
7995
8294
|
this.status = status;
|
|
7996
8295
|
}
|
|
7997
8296
|
};
|
|
8297
|
+
s3RequestTimeoutMs = DEFAULT_S3_REQUEST_TIMEOUT_MS;
|
|
8298
|
+
s3DockerCurlTimeoutMs = DEFAULT_S3_DOCKER_CURL_TIMEOUT_MS;
|
|
7998
8299
|
});
|
|
7999
8300
|
|
|
8000
8301
|
// web-src/server/database/handle-s3.ts
|
|
@@ -10889,6 +11190,13 @@ function handleSettings() {
|
|
|
10889
11190
|
}
|
|
10890
11191
|
});
|
|
10891
11192
|
}
|
|
11193
|
+
function worktreeWatchDirectoryLimitFromEnv() {
|
|
11194
|
+
const raw = process.env.CODE_VIEWER_WORKTREE_WATCH_LIMIT;
|
|
11195
|
+
if (!raw)
|
|
11196
|
+
return DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
|
|
11197
|
+
const parsed = Number(raw);
|
|
11198
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : DEFAULT_WORKTREE_WATCH_DIRECTORY_LIMIT;
|
|
11199
|
+
}
|
|
10892
11200
|
function handleFiles2(url) {
|
|
10893
11201
|
const target = url.searchParams.get("ref") || url.searchParams.get("target") || "worktree";
|
|
10894
11202
|
if (target !== "worktree" && !verifyTreeRef(target, cwd))
|
|
@@ -12237,6 +12545,7 @@ data: ok
|
|
|
12237
12545
|
excludeNames: scopeExcludeNames,
|
|
12238
12546
|
watch,
|
|
12239
12547
|
initialScanMode: "async",
|
|
12548
|
+
maxWatchedDirectories: worktreeWatchDirectoryLimitFromEnv(),
|
|
12240
12549
|
onUpdate: triggerUpdate,
|
|
12241
12550
|
onError: (error) => {
|
|
12242
12551
|
const message = error instanceof Error ? error.message : String(error);
|