@poe-platform/safe-js 0.1.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/LICENSE +21 -0
- package/README.md +206 -0
- package/dist/agent-defs/agents/claude-code.d.ts +2 -0
- package/dist/agent-defs/agents/claude-desktop.d.ts +2 -0
- package/dist/agent-defs/agents/codex.d.ts +2 -0
- package/dist/agent-defs/agents/cursor.d.ts +2 -0
- package/dist/agent-defs/agents/gemini-cli.d.ts +2 -0
- package/dist/agent-defs/agents/goose.d.ts +2 -0
- package/dist/agent-defs/agents/index.d.ts +10 -0
- package/dist/agent-defs/agents/kimi.d.ts +2 -0
- package/dist/agent-defs/agents/opencode.d.ts +2 -0
- package/dist/agent-defs/agents/pi.d.ts +2 -0
- package/dist/agent-defs/agents/poe-agent.d.ts +2 -0
- package/dist/agent-defs/capabilities.d.ts +14 -0
- package/dist/agent-defs/index.d.ts +6 -0
- package/dist/agent-defs/registry.d.ts +3 -0
- package/dist/agent-defs/specifier.d.ts +7 -0
- package/dist/agent-defs/types.d.ts +35 -0
- package/dist/agent-harness-tools/binary-exists.d.ts +10 -0
- package/dist/agent-harness-tools/execution-env.d.ts +108 -0
- package/dist/agent-harness-tools/hooks.d.ts +30 -0
- package/dist/agent-harness-tools/index.d.ts +18 -0
- package/dist/agent-harness-tools/log-stream.d.ts +28 -0
- package/dist/agent-harness-tools/participant.d.ts +10 -0
- package/dist/agent-harness-tools/paths.d.ts +14 -0
- package/dist/agent-harness-tools/plan-readiness.d.ts +7 -0
- package/dist/agent-harness-tools/plans.d.ts +35 -0
- package/dist/agent-harness-tools/poe-command-execution.d.ts +37 -0
- package/dist/agent-harness-tools/run-logs.d.ts +17 -0
- package/dist/agent-harness-tools/run-poe-command.d.ts +33 -0
- package/dist/agent-harness-tools/runner.d.ts +33 -0
- package/dist/agent-harness-tools/select-agent.d.ts +18 -0
- package/dist/agent-harness-tools/sequence.d.ts +7 -0
- package/dist/agent-harness-tools/skill-config.d.ts +1 -0
- package/dist/agent-harness-tools/stage.d.ts +22 -0
- package/dist/agent-harness-tools/workspace-transfer.d.ts +3 -0
- package/dist/agent-spawn/acp/middleware.d.ts +38 -0
- package/dist/agent-spawn/acp/types.d.ts +180 -0
- package/dist/agent-spawn/adapters/claude.d.ts +3 -0
- package/dist/agent-spawn/adapters/codex.d.ts +2 -0
- package/dist/agent-spawn/adapters/cursor.d.ts +2 -0
- package/dist/agent-spawn/adapters/index.d.ts +12 -0
- package/dist/agent-spawn/adapters/kimi.d.ts +2 -0
- package/dist/agent-spawn/adapters/native.d.ts +5 -0
- package/dist/agent-spawn/adapters/opencode.d.ts +2 -0
- package/dist/agent-spawn/adapters/pi.d.ts +2 -0
- package/dist/agent-spawn/environment.d.ts +2 -0
- package/dist/agent-spawn/parallel.d.ts +30 -0
- package/dist/agent-spawn/retry.d.ts +26 -0
- package/dist/agent-spawn/types.d.ts +272 -0
- package/dist/auth-store/create-secret-store.d.ts +2 -0
- package/dist/auth-store/encrypted-file-store.d.ts +47 -0
- package/dist/auth-store/index.d.ts +7 -0
- package/dist/auth-store/keychain-store.d.ts +25 -0
- package/dist/auth-store/provider-store.d.ts +14 -0
- package/dist/auth-store/types.d.ts +22 -0
- package/dist/config-mutations/execution/run-mutations.d.ts +7 -0
- package/dist/config-mutations/fs-utils.d.ts +18 -0
- package/dist/config-mutations/index.d.ts +8 -0
- package/dist/config-mutations/mutations/config-mutation.d.ts +47 -0
- package/dist/config-mutations/mutations/file-mutation.d.ts +62 -0
- package/dist/config-mutations/mutations/template-mutation.d.ts +40 -0
- package/dist/config-mutations/template/render.d.ts +6 -0
- package/dist/config-mutations/types.d.ts +168 -0
- package/dist/poe-code-config/compile/schema-compiler.d.ts +11 -0
- package/dist/poe-code-config/config.d.ts +2 -0
- package/dist/poe-code-config/configured-services.d.ts +29 -0
- package/dist/poe-code-config/index.d.ts +16 -0
- package/dist/poe-code-config/inspect.d.ts +13 -0
- package/dist/poe-code-config/memory.d.ts +14 -0
- package/dist/poe-code-config/merge-callbacks.d.ts +38 -0
- package/dist/poe-code-config/merge.d.ts +2 -0
- package/dist/poe-code-config/plan-scope.d.ts +8 -0
- package/dist/poe-code-config/provider-config.d.ts +16 -0
- package/dist/poe-code-config/resolve.d.ts +2 -0
- package/dist/poe-code-config/runtime.d.ts +114 -0
- package/dist/poe-code-config/schema.d.ts +14 -0
- package/dist/poe-code-config/state/fs.d.ts +24 -0
- package/dist/poe-code-config/state/index.d.ts +12 -0
- package/dist/poe-code-config/state/jobs.d.ts +34 -0
- package/dist/poe-code-config/state/templates.d.ts +18 -0
- package/dist/poe-code-config/store.d.ts +12 -0
- package/dist/poe-code-config/types.d.ts +88 -0
- package/dist/process-runner/docker/build-context.d.ts +5 -0
- package/dist/process-runner/docker/context.d.ts +3 -0
- package/dist/process-runner/docker/docker-execution-env.d.ts +28 -0
- package/dist/process-runner/docker/docker-runner.d.ts +2 -0
- package/dist/process-runner/docker/engine.d.ts +3 -0
- package/dist/process-runner/host/host-execution-env.d.ts +2 -0
- package/dist/process-runner/host/host-runner.d.ts +3 -0
- package/dist/process-runner/index.d.ts +10 -0
- package/dist/process-runner/testing/index.d.ts +2 -0
- package/dist/process-runner/testing/mock-runner.d.ts +3 -0
- package/dist/process-runner/types.d.ts +191 -0
- package/dist/process-runner/workspace-transfer.d.ts +61 -0
- package/dist/providers/auth/api-key.d.ts +5 -0
- package/dist/providers/auth/types.d.ts +14 -0
- package/dist/providers/compatibility.d.ts +4 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/providers/anthropic.d.ts +1 -0
- package/dist/providers/providers/cloudflare.d.ts +1 -0
- package/dist/providers/providers/generated.d.ts +5 -0
- package/dist/providers/providers/openai.d.ts +1 -0
- package/dist/providers/providers/poe.d.ts +2 -0
- package/dist/providers/registry.d.ts +44 -0
- package/dist/providers/types.d.ts +54 -0
- package/dist/safe-fs/bridge/filesystem.d.ts +88 -0
- package/dist/safe-fs/bridge/index.d.ts +6 -0
- package/dist/safe-fs/bridge/types.d.ts +75 -0
- package/dist/safe-fs/config.d.ts +15 -0
- package/dist/safe-fs/config.node.d.ts +2 -0
- package/dist/safe-fs/contracts/errors.d.ts +50 -0
- package/dist/safe-fs/contracts/filesystem.d.ts +96 -0
- package/dist/safe-fs/contracts/index.d.ts +4 -0
- package/dist/safe-fs/contracts/io.d.ts +8 -0
- package/dist/safe-fs/contracts/path.d.ts +8 -0
- package/dist/safe-fs/contracts/portable-path.d.ts +13 -0
- package/dist/safe-fs/contracts/virtual-path.d.ts +7 -0
- package/dist/safe-fs/core.d.ts +12 -0
- package/dist/safe-fs/fs/memory/index.d.ts +58 -0
- package/dist/safe-fs/fs/mount/comparison.d.ts +19 -0
- package/dist/safe-fs/fs/mount/index.d.ts +44 -0
- package/dist/safe-fs/fs/overlay/index.d.ts +73 -0
- package/dist/safe-fs/fs/readonly/index.d.ts +31 -0
- package/dist/safe-fs/fs/real/index.d.ts +76 -0
- package/dist/safe-fs/fs/s3/filesystem.d.ts +93 -0
- package/dist/safe-fs/fs/s3/http/index.d.ts +2 -0
- package/dist/safe-fs/fs/s3/http/transport.d.ts +3 -0
- package/dist/safe-fs/fs/s3/http/types.d.ts +30 -0
- package/dist/safe-fs/fs/s3/index.d.ts +6 -0
- package/dist/safe-fs/fs/s3/mock.d.ts +48 -0
- package/dist/safe-fs/fs/s3/transport.d.ts +103 -0
- package/dist/safe-fs/fs/webdav/index.d.ts +2 -0
- package/dist/safe-fs/fs/webdav/webdav.d.ts +101 -0
- package/dist/safe-fs/index.d.ts +14 -0
- package/dist/safe-fs/node/filesystem.d.ts +11 -0
- package/dist/safe-fs/node/index.d.ts +2 -0
- package/dist/safe-fs/node-host.d.ts +1 -0
- package/dist/safe-fs/node-unavailable.d.ts +1 -0
- package/dist/safe-fs/platform/browser.d.ts +12 -0
- package/dist/safe-fs/platform/node.d.ts +13 -0
- package/dist/safe-js/browser/chunks/chunk-QGN6DKT5.js +4335 -0
- package/dist/safe-js/browser/chunks/chunk-QGN6DKT5.js.map +7 -0
- package/dist/safe-js/browser/safe-fs-core.js +67 -0
- package/dist/safe-js/browser/safe-fs-core.js.map +7 -0
- package/dist/safe-js/browser/safe-fs.js +67 -0
- package/dist/safe-js/browser/safe-fs.js.map +7 -0
- package/dist/safe-js/chunks/chunk-2QRYPKQQ.js +4315 -0
- package/dist/safe-js/chunks/chunk-2QRYPKQQ.js.map +7 -0
- package/dist/safe-js/chunks/chunk-4YIDX44G.js +40 -0
- package/dist/safe-js/chunks/chunk-4YIDX44G.js.map +7 -0
- package/dist/safe-js/chunks/chunk-7M2SJPSZ.js +2613 -0
- package/dist/safe-js/chunks/chunk-7M2SJPSZ.js.map +7 -0
- package/dist/safe-js/chunks/chunk-DC5B3GYM.js +8248 -0
- package/dist/safe-js/chunks/chunk-DC5B3GYM.js.map +7 -0
- package/dist/safe-js/chunks/chunk-Z3D76J4U.js +30732 -0
- package/dist/safe-js/chunks/chunk-Z3D76J4U.js.map +7 -0
- package/dist/safe-js/cli.d.ts +36 -0
- package/dist/safe-js/cli.js +724 -0
- package/dist/safe-js/cli.js.map +7 -0
- package/dist/safe-js/core.d.ts +6 -0
- package/dist/safe-js/core.js +14 -0
- package/dist/safe-js/core.js.map +7 -0
- package/dist/safe-js/dump.d.ts +2 -0
- package/dist/safe-js/error/format.d.ts +15 -0
- package/dist/safe-js/error/shape.d.ts +28 -0
- package/dist/safe-js/index.d.ts +56 -0
- package/dist/safe-js/index.js +454 -0
- package/dist/safe-js/index.js.map +7 -0
- package/dist/safe-js/interp/arguments.d.ts +10 -0
- package/dist/safe-js/interp/async.d.ts +56 -0
- package/dist/safe-js/interp/budget.d.ts +95 -0
- package/dist/safe-js/interp/exceptions.d.ts +45 -0
- package/dist/safe-js/interp/generator.d.ts +16 -0
- package/dist/safe-js/interp/globals/console-json.d.ts +14 -0
- package/dist/safe-js/interp/host-bridge.d.ts +29 -0
- package/dist/safe-js/interp/host-call.d.ts +130 -0
- package/dist/safe-js/interp/interpreter.d.ts +63 -0
- package/dist/safe-js/interp/promise-replay.d.ts +70 -0
- package/dist/safe-js/interp/regex/compile-guard.d.ts +33 -0
- package/dist/safe-js/interp/regex/parse.d.ts +62 -0
- package/dist/safe-js/interp/scope.d.ts +43 -0
- package/dist/safe-js/interp/values.d.ts +136 -0
- package/dist/safe-js/lint/index.d.ts +38 -0
- package/dist/safe-js/lint/rules/AS-export-import-meta.d.ts +21 -0
- package/dist/safe-js/lint/rules/module-registry.d.ts +33 -0
- package/dist/safe-js/lint.d.ts +1 -0
- package/dist/safe-js/loader/extract-block.d.ts +10 -0
- package/dist/safe-js/loader/find-exported.d.ts +2 -0
- package/dist/safe-js/loader/frontmatter.d.ts +5 -0
- package/dist/safe-js/migrate.d.ts +33 -0
- package/dist/safe-js/migration-file.d.ts +55 -0
- package/dist/safe-js/modules/agent.d.ts +132 -0
- package/dist/safe-js/modules/env.d.ts +14 -0
- package/dist/safe-js/modules/fail.d.ts +6 -0
- package/dist/safe-js/modules/fs-config.d.ts +12 -0
- package/dist/safe-js/modules/fs.d.ts +82 -0
- package/dist/safe-js/modules/harness.d.ts +19 -0
- package/dist/safe-js/modules/log.d.ts +16 -0
- package/dist/safe-js/modules/mcp-transport.d.ts +26 -0
- package/dist/safe-js/modules/mcp.d.ts +63 -0
- package/dist/safe-js/modules/metric.d.ts +4 -0
- package/dist/safe-js/modules/registry.d.ts +16 -0
- package/dist/safe-js/modules/time.d.ts +12 -0
- package/dist/safe-js/observability/otel.d.ts +21 -0
- package/dist/safe-js/output-stream.d.ts +12 -0
- package/dist/safe-js/parse/parse-export.d.ts +36 -0
- package/dist/safe-js/parse/parser.d.ts +356 -0
- package/dist/safe-js/parse/tokenizer.d.ts +26 -0
- package/dist/safe-js/parse.d.ts +4 -0
- package/dist/safe-js/random.d.ts +9 -0
- package/dist/safe-js/restore.d.ts +26 -0
- package/dist/safe-js/run.d.ts +74 -0
- package/dist/safe-js/runner/run-harness.d.ts +32 -0
- package/dist/safe-js/safe-fs-core.js +130 -0
- package/dist/safe-js/safe-fs-core.js.map +7 -0
- package/dist/safe-js/safe-fs-node.js +94 -0
- package/dist/safe-js/safe-fs-node.js.map +7 -0
- package/dist/safe-js/safe-fs.js +94 -0
- package/dist/safe-js/safe-fs.js.map +7 -0
- package/dist/safe-js/snapshot/arguments.d.ts +17 -0
- package/dist/safe-js/snapshot/backend.d.ts +19 -0
- package/dist/safe-js/snapshot/dump-format.d.ts +7 -0
- package/dist/safe-js/snapshot/dump.d.ts +23 -0
- package/dist/safe-js/snapshot/float32array.d.ts +12 -0
- package/dist/safe-js/snapshot/migration.d.ts +35 -0
- package/dist/safe-js/snapshot/policy.d.ts +49 -0
- package/dist/safe-js/snapshot/replay-data.d.ts +71 -0
- package/dist/safe-js/snapshot/validation.d.ts +15 -0
- package/dist/task-list/backends/gh-issues-client.d.ts +19 -0
- package/dist/task-list/backends/gh-issues-sync.d.ts +46 -0
- package/dist/task-list/backends/gh-issues.d.ts +21 -0
- package/dist/task-list/index.d.ts +9 -0
- package/dist/task-list/move.d.ts +2 -0
- package/dist/task-list/open.d.ts +5 -0
- package/dist/task-list/state-machine.d.ts +16 -0
- package/dist/task-list/state.d.ts +29 -0
- package/dist/task-list/types.d.ts +206 -0
- package/dist/tiny-mcp-client/index.d.ts +662 -0
- package/dist/tiny-stdio-mcp-server/content/audio.d.ts +15 -0
- package/dist/tiny-stdio-mcp-server/content/convert.d.ts +16 -0
- package/dist/tiny-stdio-mcp-server/content/file-type.d.ts +11 -0
- package/dist/tiny-stdio-mcp-server/content/file.d.ts +28 -0
- package/dist/tiny-stdio-mcp-server/content/image.d.ts +15 -0
- package/dist/tiny-stdio-mcp-server/content/index.d.ts +7 -0
- package/dist/tiny-stdio-mcp-server/content/mime.d.ts +7 -0
- package/dist/tiny-stdio-mcp-server/content/remote.d.ts +5 -0
- package/dist/tiny-stdio-mcp-server/index.d.ts +9 -0
- package/dist/tiny-stdio-mcp-server/schema.d.ts +20 -0
- package/dist/tiny-stdio-mcp-server/server.d.ts +35 -0
- package/dist/tiny-stdio-mcp-server/types.d.ts +247 -0
- package/dist/toolcraft-design/acp/components.d.ts +19 -0
- package/dist/toolcraft-design/acp/index.d.ts +4 -0
- package/dist/toolcraft-design/acp/writer.d.ts +13 -0
- package/dist/toolcraft-design/components/browser.d.ts +15 -0
- package/dist/toolcraft-design/components/catalog.d.ts +26 -0
- package/dist/toolcraft-design/components/color.d.ts +31 -0
- package/dist/toolcraft-design/components/command-errors.d.ts +18 -0
- package/dist/toolcraft-design/components/detail-card.d.ts +22 -0
- package/dist/toolcraft-design/components/file-changes.d.ts +16 -0
- package/dist/toolcraft-design/components/help-formatter-plain.d.ts +5 -0
- package/dist/toolcraft-design/components/help-formatter.d.ts +50 -0
- package/dist/toolcraft-design/components/inspector-card.d.ts +21 -0
- package/dist/toolcraft-design/components/logger.d.ts +11 -0
- package/dist/toolcraft-design/components/resource-browser.d.ts +21 -0
- package/dist/toolcraft-design/components/symbols.d.ts +12 -0
- package/dist/toolcraft-design/components/table.d.ts +16 -0
- package/dist/toolcraft-design/components/template.d.ts +19 -0
- package/dist/toolcraft-design/components/text.d.ts +16 -0
- package/dist/toolcraft-design/dashboard/buffer.d.ts +25 -0
- package/dist/toolcraft-design/dashboard/components/footer.d.ts +8 -0
- package/dist/toolcraft-design/dashboard/dashboard.d.ts +20 -0
- package/dist/toolcraft-design/dashboard/index.d.ts +8 -0
- package/dist/toolcraft-design/dashboard/should-use-dashboard.d.ts +10 -0
- package/dist/toolcraft-design/dashboard/snapshot.d.ts +10 -0
- package/dist/toolcraft-design/dashboard/terminal.d.ts +37 -0
- package/dist/toolcraft-design/dashboard/types.d.ts +38 -0
- package/dist/toolcraft-design/explorer/events.d.ts +77 -0
- package/dist/toolcraft-design/explorer/index.d.ts +8 -0
- package/dist/toolcraft-design/explorer/keymap.d.ts +31 -0
- package/dist/toolcraft-design/explorer/runtime.d.ts +2 -0
- package/dist/toolcraft-design/explorer/state.d.ts +224 -0
- package/dist/toolcraft-design/index.d.ts +62 -0
- package/dist/toolcraft-design/internal/output-format.d.ts +6 -0
- package/dist/toolcraft-design/internal/strip-ansi.d.ts +1 -0
- package/dist/toolcraft-design/internal/theme-detect.d.ts +12 -0
- package/dist/toolcraft-design/internal/theme-state.d.ts +12 -0
- package/dist/toolcraft-design/prompts/index.d.ts +91 -0
- package/dist/toolcraft-design/prompts/interactive/cancel-symbol.d.ts +2 -0
- package/dist/toolcraft-design/prompts/interactive/select.d.ts +18 -0
- package/dist/toolcraft-design/prompts/primitives/cancel.d.ts +2 -0
- package/dist/toolcraft-design/prompts/primitives/intro.d.ts +1 -0
- package/dist/toolcraft-design/prompts/primitives/log.d.ts +18 -0
- package/dist/toolcraft-design/prompts/primitives/note.d.ts +1 -0
- package/dist/toolcraft-design/prompts/primitives/outro.d.ts +1 -0
- package/dist/toolcraft-design/prompts/primitives/spinner.d.ts +6 -0
- package/dist/toolcraft-design/prompts/theme.d.ts +11 -0
- package/dist/toolcraft-design/screen/screen.d.ts +40 -0
- package/dist/toolcraft-design/screen/style.d.ts +16 -0
- package/dist/toolcraft-design/static/index.d.ts +4 -0
- package/dist/toolcraft-design/static/menu.d.ts +11 -0
- package/dist/toolcraft-design/static/spinner.d.ts +14 -0
- package/dist/toolcraft-design/terminal/driver.d.ts +34 -0
- package/dist/toolcraft-design/terminal/input.d.ts +25 -0
- package/dist/toolcraft-design/terminal-markdown/ast.d.ts +98 -0
- package/dist/toolcraft-design/terminal-markdown/html-renderer.d.ts +7 -0
- package/dist/toolcraft-design/terminal-markdown/index.d.ts +12 -0
- package/dist/toolcraft-design/terminal-markdown/parser.d.ts +5 -0
- package/dist/toolcraft-design/terminal-markdown/plaintext-renderer.d.ts +11 -0
- package/dist/toolcraft-design/terminal-markdown/renderer.d.ts +7 -0
- package/dist/toolcraft-design/tokens/brand.d.ts +5 -0
- package/dist/toolcraft-design/tokens/colors.d.ts +37 -0
- package/dist/toolcraft-design/tokens/index.d.ts +6 -0
- package/dist/toolcraft-design/tokens/spacing.d.ts +6 -0
- package/dist/toolcraft-design/tokens/typography.d.ts +7 -0
- package/dist/toolcraft-design/tokens/widths.d.ts +5 -0
- package/dist/toolcraft-schema/index.d.ts +184 -0
- package/dist/toolcraft-schema/json-schema/index.d.ts +5 -0
- package/dist/toolcraft-schema/json-schema/types.d.ts +33 -0
- package/dist/toolcraft-schema/json-schema-document.d.ts +14 -0
- package/dist/toolcraft-schema/json.d.ts +10 -0
- package/dist/toolcraft-schema/oneof.d.ts +15 -0
- package/dist/toolcraft-schema/record.d.ts +5 -0
- package/dist/toolcraft-schema/union.d.ts +8 -0
- package/dist/toolcraft-schema/validate.d.ts +17 -0
- package/package.json +89 -0
|
@@ -0,0 +1,2613 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FileSystemBridge,
|
|
3
|
+
FsError,
|
|
4
|
+
collectBytes,
|
|
5
|
+
compareEntries,
|
|
6
|
+
compareOwnedS3Entries,
|
|
7
|
+
composeAbortSignals,
|
|
8
|
+
createMemoryFileSystem,
|
|
9
|
+
finishCleanup,
|
|
10
|
+
isErrnoCode,
|
|
11
|
+
isFsError,
|
|
12
|
+
readBytes,
|
|
13
|
+
recordS3Observation,
|
|
14
|
+
registerEntryAuthority,
|
|
15
|
+
registerS3EntryOwner,
|
|
16
|
+
toByteSource,
|
|
17
|
+
toFsError,
|
|
18
|
+
validatePath
|
|
19
|
+
} from "./chunk-2QRYPKQQ.js";
|
|
20
|
+
|
|
21
|
+
// packages/safe-fs/src/contracts/path.ts
|
|
22
|
+
import { posix } from "node:path";
|
|
23
|
+
var posixPath = posix;
|
|
24
|
+
var basename = posix.basename;
|
|
25
|
+
var dirname = posix.dirname;
|
|
26
|
+
var extname = posix.extname;
|
|
27
|
+
var joinPath = posix.join;
|
|
28
|
+
var isAbsolutePath = posix.isAbsolute;
|
|
29
|
+
|
|
30
|
+
// packages/safe-fs/src/fs/real/index.ts
|
|
31
|
+
import { constants } from "node:fs";
|
|
32
|
+
import * as native from "node:fs/promises";
|
|
33
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
34
|
+
|
|
35
|
+
// packages/safe-fs/src/fs/real/allocation.ts
|
|
36
|
+
function nativeAllocatedBytes(blocks, platform) {
|
|
37
|
+
if (platform !== "darwin" && platform !== "linux") return void 0;
|
|
38
|
+
if (typeof blocks !== "number" || !Number.isSafeInteger(blocks) || blocks < 0) return void 0;
|
|
39
|
+
const bytes = blocks * 512;
|
|
40
|
+
return Number.isSafeInteger(bytes) ? bytes : void 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// packages/safe-fs/src/fs/real/index.ts
|
|
44
|
+
function fileType(stats) {
|
|
45
|
+
if (stats.isFile()) return "file";
|
|
46
|
+
if (stats.isDirectory()) return "directory";
|
|
47
|
+
if (stats.isSymbolicLink()) return "symlink";
|
|
48
|
+
throw new FsError("ENOTSUP", { message: "special filesystem nodes are not supported" });
|
|
49
|
+
}
|
|
50
|
+
function fileStat(stats) {
|
|
51
|
+
const allocatedBytes = nativeAllocatedBytes(stats.blocks, process.platform);
|
|
52
|
+
return {
|
|
53
|
+
type: fileType(stats),
|
|
54
|
+
size: stats.size,
|
|
55
|
+
mode: stats.mode,
|
|
56
|
+
...allocatedBytes === void 0 ? {} : { allocatedBytes },
|
|
57
|
+
atimeMs: stats.atimeMs,
|
|
58
|
+
mtimeMs: stats.mtimeMs,
|
|
59
|
+
ctimeMs: stats.ctimeMs,
|
|
60
|
+
birthtimeMs: stats.birthtimeMs,
|
|
61
|
+
ino: stats.ino,
|
|
62
|
+
dev: stats.dev,
|
|
63
|
+
...Number.isSafeInteger(stats.dev) && stats.dev >= 0 && Number.isSafeInteger(stats.ino) && stats.ino >= 0 ? { identityScope: /* @__PURE__ */ Symbol.for("virtual-bash.fs.native") } : {},
|
|
64
|
+
nlink: stats.nlink,
|
|
65
|
+
uid: stats.uid,
|
|
66
|
+
gid: stats.gid
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function integer(value, minimum = 0) {
|
|
70
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
71
|
+
throw new FsError("EINVAL", { message: "expected a nonnegative safe integer" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function nativeError(error) {
|
|
75
|
+
if (typeof error === "object" && error !== null && "code" in error && (error.code === "ERR_INVALID_ARG_TYPE" || error.code === "ERR_INVALID_ARG_VALUE" || error.code === "ERR_OUT_OF_RANGE")) {
|
|
76
|
+
return new FsError("EINVAL", { cause: error });
|
|
77
|
+
}
|
|
78
|
+
if (typeof error === "object" && error !== null && "info" in error) {
|
|
79
|
+
const info = error.info;
|
|
80
|
+
if (typeof info === "object" && info !== null && "code" in info && isErrnoCode(info.code)) {
|
|
81
|
+
return new FsError(info.code, { cause: error });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return toFsError(error);
|
|
85
|
+
}
|
|
86
|
+
var RealFileSystem = class {
|
|
87
|
+
capabilities = Object.freeze({
|
|
88
|
+
readOnly: false,
|
|
89
|
+
symlinks: true,
|
|
90
|
+
hardlinks: true,
|
|
91
|
+
permissions: true,
|
|
92
|
+
timestamps: true,
|
|
93
|
+
atomicRename: true,
|
|
94
|
+
streamingRead: true,
|
|
95
|
+
streamingWrite: true
|
|
96
|
+
});
|
|
97
|
+
configuredRoot;
|
|
98
|
+
rootPromise;
|
|
99
|
+
constructor(options) {
|
|
100
|
+
const root = typeof options === "string" ? options : options.root;
|
|
101
|
+
validatePath(root);
|
|
102
|
+
if (!isAbsolute(root)) {
|
|
103
|
+
throw new FsError("EINVAL", { syscall: "root", path: root, message: "root must be an absolute host path" });
|
|
104
|
+
}
|
|
105
|
+
if (sep !== "/") {
|
|
106
|
+
throw new FsError("ENOTSUP", { syscall: "root", message: "this backend requires a POSIX host" });
|
|
107
|
+
}
|
|
108
|
+
this.configuredRoot = root;
|
|
109
|
+
}
|
|
110
|
+
async root(options = {}) {
|
|
111
|
+
options.signal?.throwIfAborted();
|
|
112
|
+
this.rootPromise ??= (async () => {
|
|
113
|
+
const root2 = await native.realpath(this.configuredRoot);
|
|
114
|
+
if (!(await native.stat(root2)).isDirectory()) throw new FsError("ENOTDIR");
|
|
115
|
+
return root2;
|
|
116
|
+
})();
|
|
117
|
+
const root = await this.rootPromise;
|
|
118
|
+
options.signal?.throwIfAborted();
|
|
119
|
+
if (await native.realpath(root) !== root) throw new FsError("EACCES");
|
|
120
|
+
options.signal?.throwIfAborted();
|
|
121
|
+
if (!(await native.stat(root)).isDirectory()) throw new FsError("ENOTDIR");
|
|
122
|
+
options.signal?.throwIfAborted();
|
|
123
|
+
return root;
|
|
124
|
+
}
|
|
125
|
+
absoluteTarget(root, target) {
|
|
126
|
+
if (target === root) return [];
|
|
127
|
+
const prefix = root === "/" ? "/" : `${root}/`;
|
|
128
|
+
if (!target.startsWith(prefix)) {
|
|
129
|
+
throw new FsError("EACCES", { message: "symlink target escapes the configured root" });
|
|
130
|
+
}
|
|
131
|
+
return target.slice(prefix.length).split("/");
|
|
132
|
+
}
|
|
133
|
+
async walk(root, components, options) {
|
|
134
|
+
let current = root;
|
|
135
|
+
let links = 0;
|
|
136
|
+
const pending = [...components];
|
|
137
|
+
while (pending.length > 0) {
|
|
138
|
+
options.signal?.throwIfAborted();
|
|
139
|
+
const { name: component, fromLink } = pending.shift();
|
|
140
|
+
if (component === "" || component === ".") continue;
|
|
141
|
+
if (component === "..") {
|
|
142
|
+
if (current === root) {
|
|
143
|
+
if (fromLink) throw new FsError("EACCES", { message: "symlink target escapes the configured root" });
|
|
144
|
+
} else {
|
|
145
|
+
current = resolve(current, "..");
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const candidate = join(current, component);
|
|
150
|
+
let stats;
|
|
151
|
+
try {
|
|
152
|
+
stats = await native.lstat(candidate);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
options.signal?.throwIfAborted();
|
|
155
|
+
const code = error.code;
|
|
156
|
+
if (options.checkTarget && (code === "ENOENT" || code === "ENOTDIR")) {
|
|
157
|
+
current = candidate;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (code !== "ENOENT") throw error;
|
|
161
|
+
if (options.createDirectories && !fromLink) {
|
|
162
|
+
try {
|
|
163
|
+
options.signal?.throwIfAborted();
|
|
164
|
+
await native.mkdir(candidate, { mode: options.createDirectories.mode });
|
|
165
|
+
} catch (creationError) {
|
|
166
|
+
if (nativeError(creationError).code !== "EEXIST") throw creationError;
|
|
167
|
+
}
|
|
168
|
+
options.signal?.throwIfAborted();
|
|
169
|
+
stats = await native.lstat(candidate);
|
|
170
|
+
} else if (!options.createDirectories && options.missing === "final" && pending.every((part) => part.name === "")) {
|
|
171
|
+
return pending.length > 0 ? `${candidate}/` : candidate;
|
|
172
|
+
} else {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
options.signal?.throwIfAborted();
|
|
177
|
+
if (stats.isSymbolicLink() && (pending.length > 0 || options.followFinal !== false)) {
|
|
178
|
+
if (++links > 40) throw new FsError("ELOOP");
|
|
179
|
+
const target = await native.readlink(candidate);
|
|
180
|
+
if (isAbsolute(target)) {
|
|
181
|
+
pending.unshift(...this.absoluteTarget(root, target).map((name) => ({ name, fromLink: true })));
|
|
182
|
+
current = root;
|
|
183
|
+
} else {
|
|
184
|
+
pending.unshift(...target.split("/").map((name) => ({ name, fromLink: true })));
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (pending.length > 0 && !stats.isDirectory() && !options.checkTarget) {
|
|
189
|
+
if (options.deferTrailingSeparator && stats.isFile() && pending.every((part) => part.name === "")) {
|
|
190
|
+
return `${candidate}/`;
|
|
191
|
+
}
|
|
192
|
+
throw new FsError("ENOTDIR");
|
|
193
|
+
}
|
|
194
|
+
if (!options.checkTarget) fileType(stats);
|
|
195
|
+
current = candidate;
|
|
196
|
+
}
|
|
197
|
+
options.signal?.throwIfAborted();
|
|
198
|
+
return components.at(-1)?.name === "" && current !== root ? `${current}/` : current;
|
|
199
|
+
}
|
|
200
|
+
async path(path, options = {}) {
|
|
201
|
+
options.signal?.throwIfAborted();
|
|
202
|
+
validatePath(path);
|
|
203
|
+
if (path === "") throw new FsError("ENOENT");
|
|
204
|
+
return this.walk(await this.root(options), path.split("/").map((name) => ({ name, fromLink: false })), options);
|
|
205
|
+
}
|
|
206
|
+
async operation(syscall, path, options, action, dest) {
|
|
207
|
+
options.signal?.throwIfAborted();
|
|
208
|
+
try {
|
|
209
|
+
validatePath(path);
|
|
210
|
+
if (dest !== void 0) validatePath(dest);
|
|
211
|
+
const result = await action();
|
|
212
|
+
options.signal?.throwIfAborted();
|
|
213
|
+
return result;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
options.signal?.throwIfAborted();
|
|
216
|
+
const converted = nativeError(error);
|
|
217
|
+
throw new FsError(converted.code, {
|
|
218
|
+
syscall,
|
|
219
|
+
path,
|
|
220
|
+
...dest === void 0 ? {} : { dest }
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
protectRoot(path, root) {
|
|
225
|
+
if (resolve(path) === root) throw new FsError("EBUSY", { message: "the filesystem root cannot be removed or replaced" });
|
|
226
|
+
}
|
|
227
|
+
protectTerminal(path) {
|
|
228
|
+
const terminal = path.split("/").filter(Boolean).at(-1);
|
|
229
|
+
if (terminal === "." || terminal === "..") throw new FsError("EINVAL");
|
|
230
|
+
}
|
|
231
|
+
async readFile(path, options = {}) {
|
|
232
|
+
return this.operation("readFile", path, options, async () => {
|
|
233
|
+
const maxBytes = options.maxBytes ?? Number.MAX_SAFE_INTEGER;
|
|
234
|
+
integer(maxBytes);
|
|
235
|
+
return collectBytes(this.readStream(path, options), { maxBytes, ...options });
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async writeFile(path, data, options = {}) {
|
|
239
|
+
return this.operation("writeFile", path, options, async () => {
|
|
240
|
+
if (!(data instanceof Uint8Array)) throw new FsError("EINVAL");
|
|
241
|
+
await this.writeStream(path, toByteSource(data), options);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async appendFile(path, data, options = {}) {
|
|
245
|
+
return this.operation("appendFile", path, options, () => this.writeFile(path, data, { ...options, flag: "a" }));
|
|
246
|
+
}
|
|
247
|
+
async stat(path, options = {}) {
|
|
248
|
+
return this.operation("stat", path, options, async () => {
|
|
249
|
+
const target = await this.path(path, options);
|
|
250
|
+
options.signal?.throwIfAborted();
|
|
251
|
+
return fileStat(await native.stat(target));
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async lstat(path, options = {}) {
|
|
255
|
+
return this.operation("lstat", path, options, async () => {
|
|
256
|
+
const target = await this.path(path, { ...options, followFinal: false });
|
|
257
|
+
options.signal?.throwIfAborted();
|
|
258
|
+
return fileStat(await native.lstat(target));
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async readdir(path, options = {}) {
|
|
262
|
+
return this.operation("readdir", path, options, async () => {
|
|
263
|
+
const target = await this.path(path, options);
|
|
264
|
+
options.signal?.throwIfAborted();
|
|
265
|
+
const entries = await native.readdir(target, { withFileTypes: true });
|
|
266
|
+
return entries.map((entry) => ({ name: entry.name, type: fileType(entry) }));
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async mkdir(path, options = {}) {
|
|
270
|
+
return this.operation("mkdir", path, options, async () => {
|
|
271
|
+
if (options.mode !== void 0) integer(options.mode);
|
|
272
|
+
const target = await this.path(path, {
|
|
273
|
+
...options,
|
|
274
|
+
missing: "final",
|
|
275
|
+
followFinal: !!options.recursive,
|
|
276
|
+
deferTrailingSeparator: true,
|
|
277
|
+
...options.recursive ? { createDirectories: { mode: options.mode ?? 511 } } : {}
|
|
278
|
+
});
|
|
279
|
+
options.signal?.throwIfAborted();
|
|
280
|
+
await native.mkdir(target, { recursive: options.recursive ?? false, ...options.mode === void 0 ? {} : { mode: options.mode } });
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async rmdir(path, options = {}) {
|
|
284
|
+
return this.operation("rmdir", path, options, async () => {
|
|
285
|
+
const target = await this.path(path.replace(/\/+$/, "") || (path ? "/" : ""), { ...options, followFinal: false });
|
|
286
|
+
this.protectTerminal(path);
|
|
287
|
+
this.protectRoot(target, await this.root(options));
|
|
288
|
+
options.signal?.throwIfAborted();
|
|
289
|
+
await native.rmdir(target);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async rm(path, options = {}) {
|
|
293
|
+
return this.operation("rm", path, options, async () => {
|
|
294
|
+
let target;
|
|
295
|
+
try {
|
|
296
|
+
target = await this.path(path, { ...options, followFinal: false });
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (options.force && toFsError(error).code === "ENOENT") return;
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
this.protectTerminal(path);
|
|
302
|
+
this.protectRoot(target, await this.root(options));
|
|
303
|
+
options.signal?.throwIfAborted();
|
|
304
|
+
await native.rm(target, { recursive: options.recursive ?? false, force: options.force ?? false });
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async rename(source, destination, options = {}) {
|
|
308
|
+
return this.operation("rename", source, options, async () => {
|
|
309
|
+
const from = await this.path(source, { ...options, followFinal: false });
|
|
310
|
+
const to = await this.path(destination, { ...options, followFinal: false, missing: "final" });
|
|
311
|
+
this.protectTerminal(source);
|
|
312
|
+
this.protectTerminal(destination);
|
|
313
|
+
const root = await this.root(options);
|
|
314
|
+
this.protectRoot(from, root);
|
|
315
|
+
this.protectRoot(to, root);
|
|
316
|
+
options.signal?.throwIfAborted();
|
|
317
|
+
await native.rename(from, to);
|
|
318
|
+
}, destination);
|
|
319
|
+
}
|
|
320
|
+
async copyFile(source, destination, options = {}) {
|
|
321
|
+
return this.operation("copyFile", source, options, async () => {
|
|
322
|
+
const from = await this.path(source, options);
|
|
323
|
+
const to = await this.path(destination, { ...options, missing: "final", followFinal: !options.exclusive, deferTrailingSeparator: true });
|
|
324
|
+
let flags = options.exclusive ? constants.COPYFILE_EXCL : 0;
|
|
325
|
+
if (!to.endsWith("/")) {
|
|
326
|
+
const origin = await native.stat(from, { bigint: true });
|
|
327
|
+
let target;
|
|
328
|
+
try {
|
|
329
|
+
target = await native.lstat(to, { bigint: true });
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (nativeError(error).code !== "ENOENT") throw error;
|
|
332
|
+
}
|
|
333
|
+
if (target && options.exclusive) throw new FsError("EEXIST");
|
|
334
|
+
if (target && origin.isFile() && origin.dev === target.dev && origin.ino === target.ino) throw new FsError("EINVAL");
|
|
335
|
+
if (!target) flags |= constants.COPYFILE_EXCL;
|
|
336
|
+
}
|
|
337
|
+
options.signal?.throwIfAborted();
|
|
338
|
+
await native.copyFile(from, to, flags);
|
|
339
|
+
}, destination);
|
|
340
|
+
}
|
|
341
|
+
async realpath(path, options = {}) {
|
|
342
|
+
return this.operation("realpath", path, options, async () => {
|
|
343
|
+
const target = await this.path(path, options);
|
|
344
|
+
return `/${relative(await this.root(options), target)}`;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async access(path, mode = constants.F_OK, options = {}) {
|
|
348
|
+
return this.operation("access", path, options, async () => {
|
|
349
|
+
integer(mode);
|
|
350
|
+
if (mode > 7) throw new FsError("EINVAL");
|
|
351
|
+
const target = await this.path(path, options);
|
|
352
|
+
options.signal?.throwIfAborted();
|
|
353
|
+
await native.access(target, mode);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async readlink(path, options = {}) {
|
|
357
|
+
return this.operation("readlink", path, options, async () => {
|
|
358
|
+
const resolved = await this.path(path, { ...options, followFinal: false });
|
|
359
|
+
options.signal?.throwIfAborted();
|
|
360
|
+
const target = await native.readlink(resolved);
|
|
361
|
+
if (!isAbsolute(target)) return target;
|
|
362
|
+
return `/${this.absoluteTarget(await this.root(options), target).join("/")}`;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async symlink(target, path, options = {}) {
|
|
366
|
+
return this.operation("symlink", path, options, async () => {
|
|
367
|
+
validatePath(target);
|
|
368
|
+
if (!target) throw new FsError("ENOENT");
|
|
369
|
+
const destination = await this.path(path, { ...options, followFinal: false, missing: "final" });
|
|
370
|
+
const root = await this.root(options);
|
|
371
|
+
const stored = target.startsWith("/") ? `${root === "/" ? "" : root}${target}` : target;
|
|
372
|
+
const components = target.startsWith("/") ? target.split("/") : [...relative(root, resolve(destination, "..")).split("/"), ...target.split("/")];
|
|
373
|
+
try {
|
|
374
|
+
await this.walk(root, components.map((name) => ({ name, fromLink: true })), { ...options, checkTarget: true });
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (nativeError(error).code !== "ELOOP") throw error;
|
|
377
|
+
}
|
|
378
|
+
options.signal?.throwIfAborted();
|
|
379
|
+
await native.symlink(stored, destination);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async link(existingPath, newPath, options = {}) {
|
|
383
|
+
return this.operation("link", existingPath, options, async () => {
|
|
384
|
+
const source = await this.path(existingPath, { ...options, followFinal: false });
|
|
385
|
+
const destination = await this.path(newPath, { ...options, followFinal: false, missing: "final" });
|
|
386
|
+
options.signal?.throwIfAborted();
|
|
387
|
+
await native.link(source, destination);
|
|
388
|
+
}, newPath);
|
|
389
|
+
}
|
|
390
|
+
async chmod(path, mode, options = {}) {
|
|
391
|
+
return this.operation("chmod", path, options, async () => {
|
|
392
|
+
integer(mode);
|
|
393
|
+
const target = await this.path(path, options);
|
|
394
|
+
options.signal?.throwIfAborted();
|
|
395
|
+
await native.chmod(target, mode);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
async utimes(path, atimeMs, mtimeMs, options = {}) {
|
|
399
|
+
return this.operation("utimes", path, options, async () => {
|
|
400
|
+
if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs)) throw new FsError("EINVAL");
|
|
401
|
+
const target = await this.path(path, options);
|
|
402
|
+
options.signal?.throwIfAborted();
|
|
403
|
+
await native.utimes(target, new Date(atimeMs), new Date(mtimeMs));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async truncate(path, length = 0, options = {}) {
|
|
407
|
+
return this.operation("truncate", path, options, async () => {
|
|
408
|
+
integer(length);
|
|
409
|
+
const target = await this.path(path, options);
|
|
410
|
+
options.signal?.throwIfAborted();
|
|
411
|
+
const handle = await native.open(target, constants.O_WRONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
412
|
+
try {
|
|
413
|
+
options.signal?.throwIfAborted();
|
|
414
|
+
if (!(await handle.stat()).isFile()) throw new FsError("ENOTSUP");
|
|
415
|
+
options.signal?.throwIfAborted();
|
|
416
|
+
await handle.truncate(length);
|
|
417
|
+
} finally {
|
|
418
|
+
await handle.close();
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
async *readStream(path, options = {}) {
|
|
423
|
+
const syscall = "readStream";
|
|
424
|
+
let handle;
|
|
425
|
+
let failed = false;
|
|
426
|
+
try {
|
|
427
|
+
options.signal?.throwIfAborted();
|
|
428
|
+
const start = options.start ?? 0;
|
|
429
|
+
const end = options.endExclusive ?? Number.MAX_SAFE_INTEGER;
|
|
430
|
+
const chunkSize = options.chunkSize ?? 64 * 1024;
|
|
431
|
+
integer(start);
|
|
432
|
+
integer(end);
|
|
433
|
+
integer(chunkSize, 1);
|
|
434
|
+
if (end < start) throw new FsError("EINVAL");
|
|
435
|
+
const target = await this.path(path, options);
|
|
436
|
+
options.signal?.throwIfAborted();
|
|
437
|
+
handle = await native.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
438
|
+
options.signal?.throwIfAborted();
|
|
439
|
+
const stats = await handle.stat();
|
|
440
|
+
if (stats.isDirectory()) throw new FsError("EISDIR");
|
|
441
|
+
if (!stats.isFile()) throw new FsError("ENOTSUP");
|
|
442
|
+
let position = start;
|
|
443
|
+
while (position < end) {
|
|
444
|
+
options.signal?.throwIfAborted();
|
|
445
|
+
const bytes = new Uint8Array(Math.min(chunkSize, end - position));
|
|
446
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.byteLength, position);
|
|
447
|
+
options.signal?.throwIfAborted();
|
|
448
|
+
if (bytesRead === 0) break;
|
|
449
|
+
position += bytesRead;
|
|
450
|
+
yield bytes.subarray(0, bytesRead);
|
|
451
|
+
}
|
|
452
|
+
options.signal?.throwIfAborted();
|
|
453
|
+
} catch (error) {
|
|
454
|
+
failed = true;
|
|
455
|
+
options.signal?.throwIfAborted();
|
|
456
|
+
throw new FsError(nativeError(error).code, { syscall, path });
|
|
457
|
+
} finally {
|
|
458
|
+
await finishCleanup(async () => {
|
|
459
|
+
try {
|
|
460
|
+
await handle?.close();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw new FsError(nativeError(error).code, { syscall, path });
|
|
463
|
+
}
|
|
464
|
+
}, failed);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async writeStream(path, source, options = {}) {
|
|
468
|
+
return this.operation("writeStream", path, options, async () => {
|
|
469
|
+
const flag = options.flag ?? "w";
|
|
470
|
+
if (!["w", "wx", "a", "ax"].includes(flag)) throw new FsError("EINVAL");
|
|
471
|
+
if (options.mode !== void 0) integer(options.mode);
|
|
472
|
+
const exclusive = flag.endsWith("x");
|
|
473
|
+
const destination = await this.path(path, { ...options, missing: "final", followFinal: !exclusive, deferTrailingSeparator: true });
|
|
474
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK | (flag.startsWith("a") ? constants.O_APPEND : constants.O_TRUNC) | (exclusive ? constants.O_EXCL : 0);
|
|
475
|
+
options.signal?.throwIfAborted();
|
|
476
|
+
const handle = await native.open(destination, flags, options.mode ?? 438);
|
|
477
|
+
try {
|
|
478
|
+
options.signal?.throwIfAborted();
|
|
479
|
+
if (!(await handle.stat()).isFile()) throw new FsError("ENOTSUP");
|
|
480
|
+
options.signal?.throwIfAborted();
|
|
481
|
+
for await (const chunk of source) {
|
|
482
|
+
options.signal?.throwIfAborted();
|
|
483
|
+
if (!(chunk instanceof Uint8Array)) throw new FsError("EINVAL");
|
|
484
|
+
let offset = 0;
|
|
485
|
+
while (offset < chunk.byteLength) {
|
|
486
|
+
options.signal?.throwIfAborted();
|
|
487
|
+
const { bytesWritten } = await handle.write(chunk, offset, chunk.byteLength - offset, null);
|
|
488
|
+
if (bytesWritten === 0) throw new FsError("EIO");
|
|
489
|
+
offset += bytesWritten;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} finally {
|
|
493
|
+
await handle.close();
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
async function createRealFileSystem(options) {
|
|
499
|
+
const filesystem = new RealFileSystem(options);
|
|
500
|
+
await filesystem.stat("/");
|
|
501
|
+
return filesystem;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// packages/safe-fs/src/fs/s3/filesystem.ts
|
|
505
|
+
import { posix as posix2 } from "node:path";
|
|
506
|
+
|
|
507
|
+
// packages/safe-fs/src/fs/s3/authority.ts
|
|
508
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
509
|
+
var queries = new AsyncLocalStorage();
|
|
510
|
+
var providerHeads = /* @__PURE__ */ new WeakMap();
|
|
511
|
+
var acceptedHeads = /* @__PURE__ */ new WeakMap();
|
|
512
|
+
function recordMockS3Head(output, input, storage) {
|
|
513
|
+
const query = queries.getStore();
|
|
514
|
+
if (query && query.Bucket === input.Bucket && query.Key === input.Key) {
|
|
515
|
+
providerHeads.set(output, { query, entry: { storage, key: input.Key } });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async function queryS3Head(input, action) {
|
|
519
|
+
const query = { ...input };
|
|
520
|
+
return queries.run(query, async () => {
|
|
521
|
+
const output = await action();
|
|
522
|
+
acceptedHeads.delete(output);
|
|
523
|
+
const proof = providerHeads.get(output);
|
|
524
|
+
if (proof?.query === query) acceptedHeads.set(output, proof.entry);
|
|
525
|
+
return output;
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function recordS3Stat(filesystem, path, stat2, metadata) {
|
|
529
|
+
if (!metadata) return;
|
|
530
|
+
const entry = acceptedHeads.get(metadata);
|
|
531
|
+
acceptedHeads.delete(metadata);
|
|
532
|
+
if (entry) recordS3Observation(filesystem, path, stat2, entry);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// packages/safe-fs/src/fs/s3/transport.ts
|
|
536
|
+
function createS3Transport(client, capabilities = {}) {
|
|
537
|
+
const transport = {
|
|
538
|
+
capabilities: Object.freeze({ ...capabilities }),
|
|
539
|
+
...client.getObjectStream ? { getObjectStream: (input, options) => client.getObjectStream(input, options) } : {},
|
|
540
|
+
...client.putObjectStream ? { putObjectStream: (input, options) => client.putObjectStream(input, options) } : {},
|
|
541
|
+
headObject: (input, options) => client.headObject(input, options),
|
|
542
|
+
getObject: (input, options) => client.getObject(input, options),
|
|
543
|
+
putObject: (input, options) => client.putObject(input, options),
|
|
544
|
+
deleteObject: (input, options) => client.deleteObject(input, options),
|
|
545
|
+
copyObject: (input, options) => client.copyObject(input, options),
|
|
546
|
+
listObjectsV2: (input, options) => client.listObjectsV2(input, options)
|
|
547
|
+
};
|
|
548
|
+
return transport;
|
|
549
|
+
}
|
|
550
|
+
var S3ServiceError = class extends Error {
|
|
551
|
+
code;
|
|
552
|
+
$metadata;
|
|
553
|
+
constructor(code, status, message = code) {
|
|
554
|
+
super(message);
|
|
555
|
+
this.name = code;
|
|
556
|
+
this.code = code;
|
|
557
|
+
this.$metadata = { httpStatusCode: status };
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
function encodeCopySource(bucket, key) {
|
|
561
|
+
return [bucket, ...key.split("/")].map((part) => encodeURIComponent(part).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// packages/safe-fs/src/fs/s3/filesystem.ts
|
|
565
|
+
var S3RenameError = class extends FsError {
|
|
566
|
+
phase;
|
|
567
|
+
copiedKeys;
|
|
568
|
+
deletedKeys;
|
|
569
|
+
constructor(source, destination, phase, copied, deleted, cause) {
|
|
570
|
+
super(cause.code, {
|
|
571
|
+
syscall: "rename",
|
|
572
|
+
path: source,
|
|
573
|
+
dest: destination,
|
|
574
|
+
cause,
|
|
575
|
+
message: `non-atomic S3 rename failed during ${phase}; destination copies or partial source deletions may remain`
|
|
576
|
+
});
|
|
577
|
+
this.name = "S3RenameError";
|
|
578
|
+
this.phase = phase;
|
|
579
|
+
this.copiedKeys = Object.freeze([...copied]);
|
|
580
|
+
this.deletedKeys = Object.freeze([...deleted]);
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
function serviceCode(error) {
|
|
584
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
585
|
+
const record = error;
|
|
586
|
+
const value = record.Code ?? record.code ?? record.name;
|
|
587
|
+
return typeof value === "string" ? value : void 0;
|
|
588
|
+
}
|
|
589
|
+
function translate(error, syscall, path, precondition = "EAGAIN") {
|
|
590
|
+
if (error instanceof FsError) return error;
|
|
591
|
+
const status = error?.$metadata?.httpStatusCode;
|
|
592
|
+
const name = serviceCode(error);
|
|
593
|
+
let code = "EIO";
|
|
594
|
+
if (name === "AbortError" || status === 499) code = "ECANCELED";
|
|
595
|
+
else if (name === "NoSuchKey" || name === "NotFound" || name === "NoSuchBucket" || status === 404) code = "ENOENT";
|
|
596
|
+
else if (["AccessDenied", "InvalidAccessKeyId", "SignatureDoesNotMatch", "ExpiredToken"].includes(name ?? "") || status === 401 || status === 403) code = "EACCES";
|
|
597
|
+
else if (name === "PreconditionFailed" || status === 412) code = precondition;
|
|
598
|
+
else if (name === "ConditionalRequestConflict" || name === "SlowDown" || status === 409 || status === 429 || status === 503) code = "EAGAIN";
|
|
599
|
+
else if (name === "NotImplemented" || status === 501) code = "ENOTSUP";
|
|
600
|
+
else if (name === "EntityTooLarge") code = "EFBIG";
|
|
601
|
+
else if (name === "InvalidArgument" || name === "InvalidRequest" || status === 400) code = "EINVAL";
|
|
602
|
+
else if (name === "TimeoutError" || name === "RequestTimeout" || status === 408) code = "ETIMEDOUT";
|
|
603
|
+
return new FsError(code, { syscall, path, cause: error });
|
|
604
|
+
}
|
|
605
|
+
function fail(code, syscall, path, message) {
|
|
606
|
+
throw new FsError(code, { syscall, path, ...message === void 0 ? {} : { message } });
|
|
607
|
+
}
|
|
608
|
+
function validateLimit(value, name, minimum, maximum = Number.MAX_SAFE_INTEGER) {
|
|
609
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
610
|
+
throw new FsError("EINVAL", { message: `${name} must be an integer from ${minimum} to ${maximum}` });
|
|
611
|
+
}
|
|
612
|
+
return value;
|
|
613
|
+
}
|
|
614
|
+
function isWellFormed(value) {
|
|
615
|
+
return !/[\uD800-\uDFFF]/u.test(value);
|
|
616
|
+
}
|
|
617
|
+
var S3FileSystem = class {
|
|
618
|
+
capabilities;
|
|
619
|
+
readStream;
|
|
620
|
+
writeStream;
|
|
621
|
+
transport;
|
|
622
|
+
bucket;
|
|
623
|
+
prefix;
|
|
624
|
+
allowRename;
|
|
625
|
+
pageSize;
|
|
626
|
+
maxReadBytes;
|
|
627
|
+
maxStreamBytes;
|
|
628
|
+
maxListEntries;
|
|
629
|
+
constructor(options) {
|
|
630
|
+
if (!options?.transport || ["headObject", "getObject", "putObject", "deleteObject", "copyObject", "listObjectsV2"].some((method) => typeof options.transport[method] !== "function")) {
|
|
631
|
+
throw new FsError("EINVAL", { message: "an explicit six-operation S3 transport is required; no default credentials or network client are created" });
|
|
632
|
+
}
|
|
633
|
+
if (options.compareEntry !== void 0 && typeof options.compareEntry !== "function") {
|
|
634
|
+
throw new FsError("EINVAL", { message: "compareEntry must be a filesystem comparison callback" });
|
|
635
|
+
}
|
|
636
|
+
if (typeof options.bucket !== "string" || !options.bucket || /[/:\0]/.test(options.bucket)) {
|
|
637
|
+
throw new FsError("EINVAL", { message: "bucket must be a nonempty bucket name, not a URL or ARN" });
|
|
638
|
+
}
|
|
639
|
+
const prefix = options.prefix ?? "";
|
|
640
|
+
if (typeof prefix !== "string" || prefix.includes("\0") || !isWellFormed(prefix) || prefix !== "" && prefix.replace(/\/$/, "").split("/").some((part) => !part || part === "." || part === "..")) {
|
|
641
|
+
throw new FsError("EINVAL", { message: "prefix must be a canonical relative object-key prefix" });
|
|
642
|
+
}
|
|
643
|
+
this.prefix = prefix === "" || prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
644
|
+
if (Buffer.byteLength(this.prefix) > 1024) throw new FsError("ENAMETOOLONG", { message: "S3 prefix exceeds 1024 UTF-8 bytes" });
|
|
645
|
+
this.transport = options.transport;
|
|
646
|
+
this.bucket = options.bucket;
|
|
647
|
+
this.allowRename = options.allowNonAtomicRename ?? true;
|
|
648
|
+
this.pageSize = validateLimit(options.pageSize ?? 1e3, "pageSize", 1, 1e3);
|
|
649
|
+
this.maxReadBytes = validateLimit(options.maxReadBytes ?? 64 * 1024 * 1024, "maxReadBytes", 0);
|
|
650
|
+
this.maxStreamBytes = validateLimit(options.maxStreamBytes ?? 5e9, "maxStreamBytes", 0, 5e9);
|
|
651
|
+
this.maxListEntries = validateLimit(options.maxListEntries ?? 1e5, "maxListEntries", 1);
|
|
652
|
+
this.capabilities = Object.freeze({
|
|
653
|
+
readOnly: options.readOnly ?? false,
|
|
654
|
+
symlinks: false,
|
|
655
|
+
hardlinks: false,
|
|
656
|
+
permissions: false,
|
|
657
|
+
timestamps: options.transport.capabilities?.conditionalCopy === true || options.transport.capabilities?.conditionalPut === true,
|
|
658
|
+
atomicRename: false,
|
|
659
|
+
snapshotRmdir: true,
|
|
660
|
+
streamingRead: options.transport.capabilities?.streamingRead === true && typeof options.transport.getObjectStream === "function",
|
|
661
|
+
streamingWrite: options.transport.capabilities?.streamingWrite === true && typeof options.transport.putObjectStream === "function"
|
|
662
|
+
});
|
|
663
|
+
if (this.capabilities.streamingRead) this.readStream = this.streamRead.bind(this);
|
|
664
|
+
if (this.capabilities.streamingWrite) this.writeStream = this.streamWrite.bind(this);
|
|
665
|
+
const transport = this.transport;
|
|
666
|
+
const bucket = this.bucket;
|
|
667
|
+
const registeredPrefix = this.prefix;
|
|
668
|
+
registerS3EntryOwner(this, (path) => this.path(path), () => this.transport === transport && this.bucket === bucket && this.prefix === registeredPrefix, s3Comparison, options.compareEntry);
|
|
669
|
+
registerEntryAuthority(this, compareOwnedS3Entries);
|
|
670
|
+
}
|
|
671
|
+
async compareEntry(path, peer, peerPath, options = {}) {
|
|
672
|
+
return compareEntries(this, path, peer, peerPath, options);
|
|
673
|
+
}
|
|
674
|
+
path(input) {
|
|
675
|
+
if (typeof input !== "string" || input.includes("\0") || !isWellFormed(input)) fail("EINVAL", "resolve", input, "invalid path string");
|
|
676
|
+
const components = [];
|
|
677
|
+
for (const part of input.split("/")) {
|
|
678
|
+
if (!part || part === ".") continue;
|
|
679
|
+
if (part === "..") {
|
|
680
|
+
if (components.length === 0) fail("EACCES", "resolve", input, "path attempts to escape the configured prefix");
|
|
681
|
+
components.pop();
|
|
682
|
+
} else components.push(part);
|
|
683
|
+
}
|
|
684
|
+
const path = `/${components.join("/")}`;
|
|
685
|
+
if (Buffer.byteLength(this.key(path)) > 1024) fail("ENAMETOOLONG", "resolve", input);
|
|
686
|
+
return path;
|
|
687
|
+
}
|
|
688
|
+
key(path) {
|
|
689
|
+
return this.prefix + path.slice(1);
|
|
690
|
+
}
|
|
691
|
+
directoryKey(path) {
|
|
692
|
+
const key = path === "/" ? this.prefix : `${this.key(path)}/`;
|
|
693
|
+
if (Buffer.byteLength(key) > 1024) fail("ENAMETOOLONG", "resolve", path);
|
|
694
|
+
return key;
|
|
695
|
+
}
|
|
696
|
+
checkAbort(options, syscall, path) {
|
|
697
|
+
if (options.signal?.aborted) fail("ECANCELED", syscall, path);
|
|
698
|
+
}
|
|
699
|
+
requestOptions(options) {
|
|
700
|
+
return options.signal === void 0 ? {} : { abortSignal: options.signal };
|
|
701
|
+
}
|
|
702
|
+
async call(syscall, path, options, action, precondition) {
|
|
703
|
+
this.checkAbort(options, syscall, path);
|
|
704
|
+
let onAbort;
|
|
705
|
+
try {
|
|
706
|
+
const pending = action();
|
|
707
|
+
const result = options.signal ? await new Promise((resolve2, reject) => {
|
|
708
|
+
const signal = options.signal;
|
|
709
|
+
onAbort = () => reject(new FsError("ECANCELED", { syscall, path, cause: signal.reason }));
|
|
710
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
711
|
+
if (signal.aborted) onAbort();
|
|
712
|
+
pending.then((value) => {
|
|
713
|
+
if (signal.aborted && typeof value === "object" && value !== null && "Body" in value) this.dispose(value.Body);
|
|
714
|
+
resolve2(value);
|
|
715
|
+
}, reject);
|
|
716
|
+
}) : await pending;
|
|
717
|
+
this.checkAbort(options, syscall, path);
|
|
718
|
+
return result;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (options.signal?.aborted) fail("ECANCELED", syscall, path);
|
|
721
|
+
throw translate(error, syscall, path, precondition);
|
|
722
|
+
} finally {
|
|
723
|
+
if (onAbort) options.signal?.removeEventListener("abort", onAbort);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
writable(path, mode) {
|
|
727
|
+
if (this.capabilities.readOnly) fail("EROFS", "write", path);
|
|
728
|
+
if (mode !== void 0) validateLimit(mode, "mode", 0, 4095);
|
|
729
|
+
}
|
|
730
|
+
unsupported(operation, path) {
|
|
731
|
+
return fail("ENOTSUP", operation, path, `S3 does not support ${operation} with the configured transport`);
|
|
732
|
+
}
|
|
733
|
+
async head(key, path, options) {
|
|
734
|
+
try {
|
|
735
|
+
const input = { Bucket: this.bucket, Key: key };
|
|
736
|
+
return await this.call("headObject", path, options, () => queryS3Head(input, () => this.transport.headObject(input, this.requestOptions(options))));
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if (isFsError(error, "ENOENT") && serviceCode(error.cause) !== "NoSuchBucket") return void 0;
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
makeStat(type, metadata) {
|
|
743
|
+
const size = type === "directory" ? 0 : metadata?.ContentLength;
|
|
744
|
+
if (size === void 0 || !Number.isSafeInteger(size) || size < 0) fail("EIO", "stat", "", "transport omitted a valid object ContentLength");
|
|
745
|
+
const modified = metadata?.LastModified?.getTime() ?? 0;
|
|
746
|
+
if (!Number.isFinite(modified)) fail("EIO", "stat", "", "transport returned an invalid LastModified");
|
|
747
|
+
const timestamp2 = (key, fallback) => {
|
|
748
|
+
const value = metadata?.Metadata?.[key];
|
|
749
|
+
if (value === void 0) return fallback;
|
|
750
|
+
const parsed = Number(value);
|
|
751
|
+
if (!value || !Number.isFinite(parsed)) fail("EIO", "stat", "", "invalid virtual timestamp metadata");
|
|
752
|
+
return parsed;
|
|
753
|
+
};
|
|
754
|
+
const storedMode = metadata?.Metadata?.["virtual-bash-mode"];
|
|
755
|
+
const mode = storedMode === void 0 ? type === "directory" ? 493 : 420 : Number(storedMode);
|
|
756
|
+
if (!Number.isSafeInteger(mode) || mode < 0 || mode > 4095 || storedMode === "") fail("EIO", "stat", "", "invalid virtual mode metadata");
|
|
757
|
+
return {
|
|
758
|
+
type,
|
|
759
|
+
size,
|
|
760
|
+
mode: (type === "directory" ? 16384 : 32768) | mode,
|
|
761
|
+
mtimeMs: timestamp2("virtual-bash-mtime", modified),
|
|
762
|
+
atimeMs: timestamp2("virtual-bash-atime", 0),
|
|
763
|
+
ctimeMs: modified
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
async page(prefix, path, options, delimiter, token, maxKeys = this.pageSize) {
|
|
767
|
+
const result = await this.call("listObjectsV2", path, options, () => this.transport.listObjectsV2({
|
|
768
|
+
Bucket: this.bucket,
|
|
769
|
+
Prefix: prefix,
|
|
770
|
+
MaxKeys: maxKeys,
|
|
771
|
+
...delimiter === void 0 ? {} : { Delimiter: delimiter },
|
|
772
|
+
...token === void 0 ? {} : { ContinuationToken: token }
|
|
773
|
+
}, this.requestOptions(options)));
|
|
774
|
+
for (const item of result.Contents ?? []) {
|
|
775
|
+
if (typeof item.Key !== "string" || !item.Key.startsWith(prefix)) fail("EIO", "listObjectsV2", path, "transport returned an object outside the requested prefix");
|
|
776
|
+
this.validateObject(item, path);
|
|
777
|
+
}
|
|
778
|
+
for (const item of result.CommonPrefixes ?? []) {
|
|
779
|
+
if (delimiter === void 0 || typeof item.Prefix !== "string" || !item.Prefix.startsWith(prefix) || !item.Prefix.endsWith("/") || item.Prefix === prefix) fail("EIO", "listObjectsV2", path, "invalid common prefix");
|
|
780
|
+
this.validateKey(item.Prefix, path);
|
|
781
|
+
}
|
|
782
|
+
if (result.IsTruncated !== void 0 && typeof result.IsTruncated !== "boolean") fail("EIO", "listObjectsV2", path, "invalid IsTruncated flag");
|
|
783
|
+
return result;
|
|
784
|
+
}
|
|
785
|
+
validateKey(key, path) {
|
|
786
|
+
if (!key.startsWith(this.prefix)) fail("EIO", "listObjectsV2", path, "key escapes the configured prefix");
|
|
787
|
+
const relative2 = key.slice(this.prefix.length).replace(/\/$/, "");
|
|
788
|
+
if (!isWellFormed(key) || key.includes("\0") || Buffer.byteLength(key) > 1024 || relative2 !== "" && relative2.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
789
|
+
fail("ENOTSUP", "listObjectsV2", path, "object key cannot be represented as a canonical filesystem path");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
validateObject(object, path) {
|
|
793
|
+
const key = object.Key;
|
|
794
|
+
this.validateKey(key, path);
|
|
795
|
+
if (object.Size === void 0 || !Number.isSafeInteger(object.Size) || object.Size < 0) fail("EIO", "listObjectsV2", path, "invalid object size");
|
|
796
|
+
if (key.endsWith("/") && object.Size !== 0) fail("ENOTSUP", "listObjectsV2", path, "nonempty slash-suffixed objects cannot be treated as directories");
|
|
797
|
+
}
|
|
798
|
+
async *pages(prefix, path, options, delimiter, maxKeys = this.pageSize) {
|
|
799
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
800
|
+
let token;
|
|
801
|
+
let entries = 0;
|
|
802
|
+
do {
|
|
803
|
+
const page = await this.page(prefix, path, options, delimiter, token, maxKeys);
|
|
804
|
+
entries += (page.Contents?.length ?? 0) + (page.CommonPrefixes?.length ?? 0);
|
|
805
|
+
if (entries > this.maxListEntries || tokens.size >= this.maxListEntries) fail("EFBIG", "listObjectsV2", path, "listing exceeds maxListEntries");
|
|
806
|
+
yield page;
|
|
807
|
+
if (!page.IsTruncated) return;
|
|
808
|
+
token = page.NextContinuationToken;
|
|
809
|
+
if (typeof token !== "string" || !token || tokens.has(token)) fail("EIO", "listObjectsV2", path, "missing or repeated continuation token");
|
|
810
|
+
tokens.add(token);
|
|
811
|
+
} while (true);
|
|
812
|
+
}
|
|
813
|
+
async inspect(path, options) {
|
|
814
|
+
if (path === "/") {
|
|
815
|
+
const listing = await this.page(this.prefix, path, options, void 0, void 0, 1);
|
|
816
|
+
const marker2 = this.prefix && listing.Contents?.some((object) => object.Key === this.prefix) ? await this.head(this.prefix, path, options) : void 0;
|
|
817
|
+
if (marker2 && marker2.ContentLength !== 0) this.unsupported("nonempty directory markers", path);
|
|
818
|
+
return { stat: this.makeStat("directory", marker2), ...marker2 ? { metadata: marker2 } : {} };
|
|
819
|
+
}
|
|
820
|
+
const file = await this.head(this.key(path), path, options);
|
|
821
|
+
if (Buffer.byteLength(this.key(path)) === 1024) {
|
|
822
|
+
return file ? { stat: this.makeStat("file", file), metadata: file } : void 0;
|
|
823
|
+
}
|
|
824
|
+
const directoryKey = this.directoryKey(path);
|
|
825
|
+
const marker = await this.head(directoryKey, path, options);
|
|
826
|
+
if (marker && marker.ContentLength !== 0) this.unsupported("nonempty directory markers", path);
|
|
827
|
+
let directory = marker !== void 0;
|
|
828
|
+
if (!directory) {
|
|
829
|
+
for await (const children2 of this.pages(directoryKey, path, options, void 0, 1)) {
|
|
830
|
+
if ((children2.Contents?.length ?? 0) > 0) {
|
|
831
|
+
directory = true;
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (file && directory) this.unsupported("file/prefix collisions", path);
|
|
837
|
+
if (file) return { stat: this.makeStat("file", file), metadata: file };
|
|
838
|
+
if (directory) return { stat: this.makeStat("directory", marker), ...marker ? { metadata: marker } : {} };
|
|
839
|
+
return void 0;
|
|
840
|
+
}
|
|
841
|
+
async lookup(path, options) {
|
|
842
|
+
const ancestors = [];
|
|
843
|
+
for (let parent = posix2.dirname(path); path !== "/"; parent = posix2.dirname(parent)) {
|
|
844
|
+
ancestors.unshift(parent);
|
|
845
|
+
if (parent === "/") break;
|
|
846
|
+
}
|
|
847
|
+
for (const parent of ancestors) {
|
|
848
|
+
const ancestor = await this.inspect(parent, options);
|
|
849
|
+
if (!ancestor) fail("ENOENT", "stat", parent);
|
|
850
|
+
if (ancestor.stat.type !== "directory") fail("ENOTDIR", "stat", parent);
|
|
851
|
+
}
|
|
852
|
+
return this.inspect(path, options);
|
|
853
|
+
}
|
|
854
|
+
requireDirectorySuffix(input, info) {
|
|
855
|
+
if (input.endsWith("/") && info?.stat.type === "file") fail("ENOTDIR", "stat", input);
|
|
856
|
+
}
|
|
857
|
+
async stat(input, options = {}) {
|
|
858
|
+
const path = this.path(input);
|
|
859
|
+
const info = await this.lookup(path, options);
|
|
860
|
+
this.requireDirectorySuffix(input, info);
|
|
861
|
+
if (!info) fail("ENOENT", "stat", input);
|
|
862
|
+
recordS3Stat(this, path, info.stat, info.metadata);
|
|
863
|
+
return info.stat;
|
|
864
|
+
}
|
|
865
|
+
async lstat(path, options = {}) {
|
|
866
|
+
return this.stat(path, options);
|
|
867
|
+
}
|
|
868
|
+
async body(output, path, options) {
|
|
869
|
+
const limit2 = Math.min(this.maxReadBytes, validateLimit(options.maxBytes ?? this.maxReadBytes, "maxBytes", 0));
|
|
870
|
+
return this.call("readFile", path, options, async () => {
|
|
871
|
+
if (output.ContentLength !== void 0 && output.ContentLength > limit2) fail("EFBIG", "readFile", path);
|
|
872
|
+
const body = output.Body;
|
|
873
|
+
if (!body) fail("EIO", "readFile", path, "transport omitted the response body");
|
|
874
|
+
let bytes;
|
|
875
|
+
if (body instanceof Uint8Array) {
|
|
876
|
+
if (body.byteLength > limit2) fail("EFBIG", "readFile", path);
|
|
877
|
+
bytes = new Uint8Array(body);
|
|
878
|
+
} else if (Symbol.asyncIterator in body) {
|
|
879
|
+
bytes = await collectBytes(body, { maxBytes: limit2, ...options.signal ? { signal: options.signal } : {} });
|
|
880
|
+
} else if ("transformToByteArray" in body) {
|
|
881
|
+
const converted = await body.transformToByteArray();
|
|
882
|
+
if (!(converted instanceof Uint8Array)) fail("EIO", "readFile", path, "transport body is not binary");
|
|
883
|
+
if (converted.byteLength > limit2) fail("EFBIG", "readFile", path);
|
|
884
|
+
bytes = new Uint8Array(converted);
|
|
885
|
+
} else fail("EIO", "readFile", path, "unsupported response body");
|
|
886
|
+
this.checkAbort(options, "readFile", path);
|
|
887
|
+
if (output.ContentLength !== void 0 && output.ContentLength !== bytes.byteLength) fail("EIO", "readFile", path, "response body length does not match ContentLength");
|
|
888
|
+
return bytes;
|
|
889
|
+
}).catch((error) => {
|
|
890
|
+
this.dispose(output.Body);
|
|
891
|
+
throw error;
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
async get(path, options) {
|
|
895
|
+
return this.call("getObject", path, options, () => this.transport.getObject({ Bucket: this.bucket, Key: this.key(path) }, this.requestOptions(options)));
|
|
896
|
+
}
|
|
897
|
+
async readFile(input, options = {}) {
|
|
898
|
+
const path = this.path(input);
|
|
899
|
+
validateLimit(options.maxBytes ?? this.maxReadBytes, "maxBytes", 0);
|
|
900
|
+
const info = await this.stat(input, options);
|
|
901
|
+
if (info.type === "directory") fail("EISDIR", "readFile", path);
|
|
902
|
+
if (info.size > Math.min(this.maxReadBytes, options.maxBytes ?? this.maxReadBytes)) fail("EFBIG", "readFile", path);
|
|
903
|
+
return this.body(await this.get(path, options), path, options);
|
|
904
|
+
}
|
|
905
|
+
etag(metadata, path) {
|
|
906
|
+
if (!metadata?.ETag) this.unsupported("conditional mutation without an ETag", path);
|
|
907
|
+
return metadata.ETag;
|
|
908
|
+
}
|
|
909
|
+
async writeFile(input, data, options = {}) {
|
|
910
|
+
const path = this.path(input);
|
|
911
|
+
this.writable(path, options.mode);
|
|
912
|
+
if (!(data instanceof Uint8Array)) fail("EINVAL", "writeFile", path, "data must be Uint8Array");
|
|
913
|
+
const bytes = new Uint8Array(data);
|
|
914
|
+
if (bytes.byteLength > 5e9) fail("EFBIG", "writeFile", path, "multipart upload is not supported");
|
|
915
|
+
const flag = options.flag ?? "w";
|
|
916
|
+
if (!["w", "wx", "a", "ax"].includes(flag)) fail("EINVAL", "writeFile", path, "invalid write flag");
|
|
917
|
+
if (flag !== "w" && !this.transport.capabilities?.conditionalPut) this.unsupported("conditional writes", path);
|
|
918
|
+
const info = await this.lookup(path, options);
|
|
919
|
+
this.requireDirectorySuffix(input, info);
|
|
920
|
+
if (info?.stat.type === "directory") fail("EISDIR", "writeFile", path);
|
|
921
|
+
if (input.endsWith("/")) fail("EISDIR", "writeFile", input);
|
|
922
|
+
const exclusive = flag === "wx" || flag === "ax";
|
|
923
|
+
if (exclusive && info) fail("EEXIST", "writeFile", path);
|
|
924
|
+
let body = bytes;
|
|
925
|
+
let match;
|
|
926
|
+
let metadata = this.writeMetadata(info?.metadata, options.mode);
|
|
927
|
+
if (flag === "a" && info) {
|
|
928
|
+
const current = await this.get(path, options);
|
|
929
|
+
const previous = await this.body(current, path, options);
|
|
930
|
+
match = this.etag(current, path);
|
|
931
|
+
metadata = { ...current.Metadata };
|
|
932
|
+
delete metadata["virtual-bash-mtime"];
|
|
933
|
+
if (previous.length + bytes.length > this.maxReadBytes) fail("EFBIG", "appendFile", path);
|
|
934
|
+
body = new Uint8Array(previous.length + bytes.length);
|
|
935
|
+
body.set(previous);
|
|
936
|
+
body.set(bytes, previous.length);
|
|
937
|
+
}
|
|
938
|
+
await this.call("putObject", path, options, () => this.transport.putObject({
|
|
939
|
+
Bucket: this.bucket,
|
|
940
|
+
Key: this.key(path),
|
|
941
|
+
Body: body,
|
|
942
|
+
...match === void 0 ? {} : { IfMatch: match },
|
|
943
|
+
...exclusive || flag === "a" && !info ? { IfNoneMatch: "*" } : {},
|
|
944
|
+
...metadata === void 0 ? {} : { Metadata: metadata }
|
|
945
|
+
}, this.requestOptions(options)), exclusive ? "EEXIST" : "EAGAIN");
|
|
946
|
+
}
|
|
947
|
+
writeMetadata(previous, mode) {
|
|
948
|
+
const metadata = { ...previous?.Metadata };
|
|
949
|
+
delete metadata["virtual-bash-mtime"];
|
|
950
|
+
if (previous === void 0 && mode !== void 0) metadata["virtual-bash-mode"] = String(mode);
|
|
951
|
+
return Object.keys(metadata).length === 0 ? void 0 : metadata;
|
|
952
|
+
}
|
|
953
|
+
async appendFile(path, data, options = {}) {
|
|
954
|
+
return this.writeFile(path, data, { ...options, flag: "a" });
|
|
955
|
+
}
|
|
956
|
+
async mkdir(input, options = {}) {
|
|
957
|
+
const path = this.path(input);
|
|
958
|
+
this.writable(path, options.mode);
|
|
959
|
+
if (options.recursive && path !== "/") {
|
|
960
|
+
await this.mkdir(posix2.dirname(path), { ...options, recursive: true });
|
|
961
|
+
}
|
|
962
|
+
const info = await this.lookup(path, options);
|
|
963
|
+
if (info) {
|
|
964
|
+
if (options.recursive && info.stat.type === "directory") return;
|
|
965
|
+
fail("EEXIST", "mkdir", path);
|
|
966
|
+
}
|
|
967
|
+
await this.call("putObject", path, options, () => this.transport.putObject({
|
|
968
|
+
Bucket: this.bucket,
|
|
969
|
+
Key: this.directoryKey(path),
|
|
970
|
+
Body: new Uint8Array(),
|
|
971
|
+
...options.mode === void 0 ? {} : { Metadata: { "virtual-bash-mode": String(options.mode) } },
|
|
972
|
+
...this.transport.capabilities?.conditionalPut ? { IfNoneMatch: "*" } : {}
|
|
973
|
+
}, this.requestOptions(options)), "EEXIST");
|
|
974
|
+
}
|
|
975
|
+
async readdir(input, options = {}) {
|
|
976
|
+
const path = this.path(input);
|
|
977
|
+
if ((await this.stat(input, options)).type !== "directory") fail("ENOTDIR", "readdir", path);
|
|
978
|
+
const prefix = this.directoryKey(path);
|
|
979
|
+
const entries = /* @__PURE__ */ new Map();
|
|
980
|
+
const add = (name, type) => {
|
|
981
|
+
if (!name || name.includes("/")) fail("EIO", "readdir", path, "invalid direct child in delimited listing");
|
|
982
|
+
const existing = entries.get(name);
|
|
983
|
+
if (existing && existing.type !== type) this.unsupported("file/prefix collisions", path);
|
|
984
|
+
entries.set(name, { name, type });
|
|
985
|
+
};
|
|
986
|
+
for await (const page of this.pages(prefix, path, options, "/")) {
|
|
987
|
+
for (const item of page.Contents ?? []) {
|
|
988
|
+
if (item.Key === prefix) continue;
|
|
989
|
+
add(item.Key.slice(prefix.length), "file");
|
|
990
|
+
}
|
|
991
|
+
for (const item of page.CommonPrefixes ?? []) add(item.Prefix.slice(prefix.length, -1), "directory");
|
|
992
|
+
}
|
|
993
|
+
return [...entries.values()].sort((left, right) => Buffer.compare(Buffer.from(left.name), Buffer.from(right.name)));
|
|
994
|
+
}
|
|
995
|
+
async tree(path, options) {
|
|
996
|
+
const objects = /* @__PURE__ */ new Map();
|
|
997
|
+
for await (const page of this.pages(this.directoryKey(path), path, options)) {
|
|
998
|
+
for (const item of page.Contents ?? []) objects.set(item.Key, item);
|
|
999
|
+
}
|
|
1000
|
+
for (const key of objects.keys()) {
|
|
1001
|
+
const withoutSlash = key.replace(/\/$/, "");
|
|
1002
|
+
if (key.endsWith("/") && objects.has(withoutSlash)) this.unsupported("file/prefix collisions", path);
|
|
1003
|
+
let parent = posix2.dirname(withoutSlash);
|
|
1004
|
+
while (parent !== "." && parent !== "/") {
|
|
1005
|
+
if (objects.has(parent)) this.unsupported("file/prefix collisions", path);
|
|
1006
|
+
parent = posix2.dirname(parent);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return [...objects.values()];
|
|
1010
|
+
}
|
|
1011
|
+
async rmdir(input, options = {}) {
|
|
1012
|
+
try {
|
|
1013
|
+
const path = this.path(input);
|
|
1014
|
+
this.checkAbort(options, "rmdir", input);
|
|
1015
|
+
this.writable(path);
|
|
1016
|
+
if (path === "/") fail("EBUSY", "rmdir", input, "cannot remove the mounted root");
|
|
1017
|
+
const info = await this.lookup(path, options);
|
|
1018
|
+
this.checkAbort(options, "rmdir", input);
|
|
1019
|
+
if (!info) fail("ENOENT", "rmdir", input);
|
|
1020
|
+
if (info.stat.type !== "directory") fail("ENOTDIR", "rmdir", input);
|
|
1021
|
+
const prefix = this.directoryKey(path);
|
|
1022
|
+
let markerObserved = false;
|
|
1023
|
+
for await (const page of this.pages(prefix, path, options, "/", Math.max(2, this.pageSize))) {
|
|
1024
|
+
this.checkAbort(options, "rmdir", input);
|
|
1025
|
+
if (page.Contents?.some((item) => item.Key !== prefix) || page.CommonPrefixes?.length) {
|
|
1026
|
+
fail("ENOTEMPTY", "rmdir", input);
|
|
1027
|
+
}
|
|
1028
|
+
if (page.IsTruncated === void 0) fail("EIO", "rmdir", input, "listing did not confirm completeness");
|
|
1029
|
+
if (page.Contents?.some((item) => item.Key === prefix)) markerObserved = true;
|
|
1030
|
+
}
|
|
1031
|
+
this.checkAbort(options, "rmdir", input);
|
|
1032
|
+
if (!info.metadata) fail("ENOTSUP", "rmdir", input, "snapshot removal requires an explicit directory marker");
|
|
1033
|
+
if (!markerObserved) fail("ENOENT", "rmdir", input, "directory marker disappeared during inspection");
|
|
1034
|
+
await this.call("deleteObject", path, options, () => this.transport.deleteObject({ Bucket: this.bucket, Key: prefix }, this.requestOptions(options)));
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
if (isFsError(error) && (error.syscall !== "rmdir" || error.path !== input)) {
|
|
1037
|
+
throw new FsError(error.code, { syscall: "rmdir", path: input, cause: error });
|
|
1038
|
+
}
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async rm(input, options = {}) {
|
|
1043
|
+
const path = this.path(input);
|
|
1044
|
+
this.writable(path);
|
|
1045
|
+
if (path === "/") fail("EBUSY", "rm", path, "cannot remove the mounted root");
|
|
1046
|
+
let info;
|
|
1047
|
+
try {
|
|
1048
|
+
info = await this.lookup(path, options);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
if (options.force && isFsError(error, "ENOENT") && serviceCode(error.cause) !== "NoSuchBucket") return;
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
this.requireDirectorySuffix(input, info);
|
|
1054
|
+
if (!info) {
|
|
1055
|
+
if (options.force) return;
|
|
1056
|
+
fail("ENOENT", "rm", path);
|
|
1057
|
+
}
|
|
1058
|
+
const objects = info.stat.type === "directory" ? await this.tree(path, options) : [{ Key: this.key(path) }];
|
|
1059
|
+
if (info.stat.type === "directory" && !options.recursive && objects.some((item) => item.Key !== this.directoryKey(path))) fail("ENOTEMPTY", "rm", path);
|
|
1060
|
+
for (const item of objects) {
|
|
1061
|
+
await this.call("deleteObject", path, options, () => this.transport.deleteObject({ Bucket: this.bucket, Key: item.Key }, this.requestOptions(options)));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async copy(sourceKey, destinationKey, source, metadata, options, destination) {
|
|
1065
|
+
if (metadata.ContentLength === void 0 || metadata.ContentLength > 5e9) this.unsupported("multipart copy", source);
|
|
1066
|
+
const etag = this.etag(metadata, source);
|
|
1067
|
+
const result = await this.call("copyObject", source, options, () => this.transport.copyObject({
|
|
1068
|
+
Bucket: this.bucket,
|
|
1069
|
+
Key: destinationKey,
|
|
1070
|
+
CopySource: encodeCopySource(this.bucket, sourceKey),
|
|
1071
|
+
CopySourceIfMatch: etag,
|
|
1072
|
+
MetadataDirective: "COPY",
|
|
1073
|
+
...options.exclusive ? { IfNoneMatch: "*" } : {},
|
|
1074
|
+
...destination === void 0 ? {} : destination === null ? { IfNoneMatch: "*" } : { IfMatch: this.etag(destination, source) }
|
|
1075
|
+
}, this.requestOptions(options)));
|
|
1076
|
+
if (!result.CopyObjectResult?.ETag) fail("EIO", "copyObject", source, "transport did not confirm a completed CopyObject result");
|
|
1077
|
+
}
|
|
1078
|
+
async copyFile(sourceInput, destinationInput, options = {}) {
|
|
1079
|
+
const source = this.path(sourceInput);
|
|
1080
|
+
const destination = this.path(destinationInput);
|
|
1081
|
+
this.writable(destination);
|
|
1082
|
+
if (options.exclusive && !this.transport.capabilities?.conditionalCopy) this.unsupported("exclusive server-side copy", destination);
|
|
1083
|
+
const origin = await this.lookup(source, options);
|
|
1084
|
+
this.requireDirectorySuffix(sourceInput, origin);
|
|
1085
|
+
if (!origin) fail("ENOENT", "copyFile", source);
|
|
1086
|
+
if (origin.stat.type === "directory") fail("EISDIR", "copyFile", source);
|
|
1087
|
+
const target = await this.lookup(destination, options);
|
|
1088
|
+
this.requireDirectorySuffix(destinationInput, target);
|
|
1089
|
+
if (target?.stat.type === "directory" || destinationInput.endsWith("/")) fail("EISDIR", "copyFile", destination);
|
|
1090
|
+
if (target && options.exclusive) fail("EEXIST", "copyFile", destination);
|
|
1091
|
+
if (source === destination) return;
|
|
1092
|
+
try {
|
|
1093
|
+
await this.copy(this.key(source), this.key(destination), source, origin.metadata, options);
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
if (!options.exclusive || !isFsError(error, "EAGAIN")) throw error;
|
|
1096
|
+
const status = error.cause?.$metadata?.httpStatusCode;
|
|
1097
|
+
if (serviceCode(error.cause) !== "PreconditionFailed" && status !== 412) throw error;
|
|
1098
|
+
let destinationExists = false;
|
|
1099
|
+
try {
|
|
1100
|
+
const currentSource = await this.head(this.key(source), source, options);
|
|
1101
|
+
if (currentSource?.ETag === origin.metadata?.ETag) {
|
|
1102
|
+
destinationExists = await this.head(this.key(destination), destination, options) !== void 0;
|
|
1103
|
+
}
|
|
1104
|
+
} catch (diagnosticError) {
|
|
1105
|
+
if (isFsError(diagnosticError, "ECANCELED")) throw diagnosticError;
|
|
1106
|
+
}
|
|
1107
|
+
if (destinationExists) {
|
|
1108
|
+
throw new FsError("EEXIST", { syscall: "copyFile", path: source, dest: destination, cause: error });
|
|
1109
|
+
}
|
|
1110
|
+
throw error;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async rename(sourceInput, destinationInput, options = {}) {
|
|
1114
|
+
const source = this.path(sourceInput);
|
|
1115
|
+
const destination = this.path(destinationInput);
|
|
1116
|
+
this.writable(destination);
|
|
1117
|
+
if (!this.allowRename) this.unsupported("atomic rename; explicitly enable allowNonAtomicRename for copy/delete semantics", source);
|
|
1118
|
+
if (!this.transport.capabilities?.conditionalDelete) this.unsupported("rename without conditional delete", source);
|
|
1119
|
+
const conditionalCopy = this.transport.capabilities?.conditionalCopy === true;
|
|
1120
|
+
if (!conditionalCopy && this.transport.capabilities?.conditionalPut !== true) {
|
|
1121
|
+
this.unsupported("rename requires conditional destination copy or conditional PUT, plus conditional delete", source);
|
|
1122
|
+
}
|
|
1123
|
+
const streamFallback = this.capabilities.streamingRead && this.capabilities.streamingWrite;
|
|
1124
|
+
if (source === "/" || destination === "/") fail("EBUSY", "rename", source);
|
|
1125
|
+
const origin = await this.lookup(source, options);
|
|
1126
|
+
this.requireDirectorySuffix(sourceInput, origin);
|
|
1127
|
+
if (!origin) fail("ENOENT", "rename", source);
|
|
1128
|
+
if (source === destination) {
|
|
1129
|
+
this.requireDirectorySuffix(destinationInput, origin);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (destination.startsWith(`${source}/`)) fail("EINVAL", "rename", destination, "cannot move a path into itself");
|
|
1133
|
+
const target = await this.lookup(destination, options);
|
|
1134
|
+
this.requireDirectorySuffix(destinationInput, target);
|
|
1135
|
+
if (destinationInput.endsWith("/") && origin.stat.type !== "directory") fail("ENOTDIR", "rename", destination);
|
|
1136
|
+
if (target && target.stat.type !== origin.stat.type) fail(target.stat.type === "directory" ? "EISDIR" : "ENOTDIR", "rename", destination);
|
|
1137
|
+
if (target?.stat.type === "directory" && (await this.readdir(destination, options)).length > 0) fail("ENOTEMPTY", "rename", destination);
|
|
1138
|
+
if (target?.metadata) this.etag(target.metadata, destination);
|
|
1139
|
+
const objects = origin.stat.type === "directory" ? await this.tree(source, options) : [{
|
|
1140
|
+
Key: this.key(source),
|
|
1141
|
+
Size: origin.stat.size,
|
|
1142
|
+
ETag: origin.metadata?.ETag
|
|
1143
|
+
}];
|
|
1144
|
+
for (const object of objects) {
|
|
1145
|
+
this.etag({ ETag: object.ETag }, source);
|
|
1146
|
+
if (object.Size === void 0 || object.Size > 5e9) this.unsupported("multipart copy", source);
|
|
1147
|
+
if (!conditionalCopy && object.Size > (streamFallback ? this.maxStreamBytes : this.maxReadBytes)) {
|
|
1148
|
+
fail("EFBIG", "rename", source, "conditional PUT fallback exceeds its configured transfer or buffered read limit");
|
|
1149
|
+
}
|
|
1150
|
+
if (Buffer.byteLength(this.key(destination) + object.Key.slice(this.key(source).length)) > 1024) fail("ENAMETOOLONG", "rename", destination);
|
|
1151
|
+
}
|
|
1152
|
+
const copied = [];
|
|
1153
|
+
const deleted = [];
|
|
1154
|
+
let phase = "copy";
|
|
1155
|
+
try {
|
|
1156
|
+
for (const object of objects) {
|
|
1157
|
+
const destinationKey = this.key(destination) + object.Key.slice(this.key(source).length);
|
|
1158
|
+
const destinationMetadata = origin.stat.type === "file" || object.Key === this.directoryKey(source) ? target?.metadata ?? null : null;
|
|
1159
|
+
const metadata = { ContentLength: object.Size, ETag: object.ETag };
|
|
1160
|
+
if (conditionalCopy) await this.copy(object.Key, destinationKey, source, metadata, options, destinationMetadata);
|
|
1161
|
+
else await this.copyWithPut(object.Key, destinationKey, source, metadata, destinationMetadata, options, streamFallback);
|
|
1162
|
+
copied.push(destinationKey);
|
|
1163
|
+
}
|
|
1164
|
+
phase = "delete";
|
|
1165
|
+
for (const object of objects) {
|
|
1166
|
+
await this.call("deleteObject", source, options, () => this.transport.deleteObject({
|
|
1167
|
+
Bucket: this.bucket,
|
|
1168
|
+
Key: object.Key,
|
|
1169
|
+
IfMatch: object.ETag
|
|
1170
|
+
}, this.requestOptions(options)));
|
|
1171
|
+
deleted.push(object.Key);
|
|
1172
|
+
}
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
throw new S3RenameError(source, destination, phase, copied, deleted, translate(error, "rename", source));
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async copyWithPut(sourceKey, destinationKey, source, expected, destination, options, streaming) {
|
|
1178
|
+
const etag = this.etag(expected, source);
|
|
1179
|
+
const condition = destination === null ? { IfNoneMatch: "*" } : { IfMatch: this.etag(destination, destinationKey) };
|
|
1180
|
+
const controller = new AbortController();
|
|
1181
|
+
const signalScope = options.signal ? composeAbortSignals([options.signal, controller.signal]) : void 0;
|
|
1182
|
+
const signal = signalScope?.signal ?? controller.signal;
|
|
1183
|
+
const operationOptions = { signal };
|
|
1184
|
+
let output;
|
|
1185
|
+
let upload;
|
|
1186
|
+
let reading = false;
|
|
1187
|
+
try {
|
|
1188
|
+
output = await this.call("getObject", source, operationOptions, () => streaming ? this.transport.getObjectStream({ Bucket: this.bucket, Key: sourceKey, IfMatch: etag }, { abortSignal: signal }) : this.transport.getObject({ Bucket: this.bucket, Key: sourceKey }, { abortSignal: signal }));
|
|
1189
|
+
if (this.etag(output, source) !== etag) fail("EAGAIN", "rename", source, "source changed before conditional PUT copy");
|
|
1190
|
+
if (output.ContentLength !== void 0 && output.ContentLength !== expected.ContentLength) {
|
|
1191
|
+
fail("EIO", "rename", source, "source response length differs from the enumerated object");
|
|
1192
|
+
}
|
|
1193
|
+
const input = {
|
|
1194
|
+
Bucket: this.bucket,
|
|
1195
|
+
Key: destinationKey,
|
|
1196
|
+
...condition,
|
|
1197
|
+
...output.Metadata === void 0 ? {} : { Metadata: { ...output.Metadata } }
|
|
1198
|
+
};
|
|
1199
|
+
if (!streaming) {
|
|
1200
|
+
const body2 = await this.body(output, source, operationOptions);
|
|
1201
|
+
if (body2.length !== expected.ContentLength) fail("EIO", "rename", source, "source body length differs from the enumerated object");
|
|
1202
|
+
await this.call("putObject", source, operationOptions, () => this.transport.putObject({ ...input, Body: body2 }, { abortSignal: signal }));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const body = output.Body;
|
|
1206
|
+
if (!body || !(Symbol.asyncIterator in body)) fail("EIO", "rename", source, "streaming transport did not return an async binary body");
|
|
1207
|
+
let finished = false;
|
|
1208
|
+
upload = (async function* () {
|
|
1209
|
+
let count = 0;
|
|
1210
|
+
reading = true;
|
|
1211
|
+
for await (const chunk of readBytes(body, signal)) {
|
|
1212
|
+
if (chunk.length > expected.ContentLength - count) fail("EIO", "rename", source, "source body exceeds its enumerated length");
|
|
1213
|
+
count += chunk.length;
|
|
1214
|
+
for (let offset = 0; offset < chunk.length; offset += 64 * 1024) {
|
|
1215
|
+
signal.throwIfAborted();
|
|
1216
|
+
yield new Uint8Array(chunk.subarray(offset, offset + 64 * 1024));
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (count !== expected.ContentLength) fail("EIO", "rename", source, "incomplete source body");
|
|
1220
|
+
finished = true;
|
|
1221
|
+
})();
|
|
1222
|
+
await this.call("putObject", source, operationOptions, () => this.transport.putObjectStream({ ...input, Body: upload }, { abortSignal: signal }));
|
|
1223
|
+
if (!finished) fail("EIO", "rename", source, "transport completed without consuming the conditional copy body");
|
|
1224
|
+
} finally {
|
|
1225
|
+
controller.abort();
|
|
1226
|
+
signalScope?.dispose();
|
|
1227
|
+
this.dispose(upload);
|
|
1228
|
+
this.dispose(output?.Body, !reading);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
async realpath(input, options = {}) {
|
|
1232
|
+
await this.stat(input, options);
|
|
1233
|
+
return this.path(input);
|
|
1234
|
+
}
|
|
1235
|
+
async access(path, mode = 0, options = {}) {
|
|
1236
|
+
if (!Number.isInteger(mode) || mode < 0 || mode > 7) fail("EINVAL", "access", path);
|
|
1237
|
+
const info = await this.stat(path, options);
|
|
1238
|
+
if ((mode & 2) !== 0 && this.capabilities.readOnly) fail("EROFS", "access", path);
|
|
1239
|
+
if ((mode & 1) !== 0 && info.type !== "directory") fail("EACCES", "access", path);
|
|
1240
|
+
}
|
|
1241
|
+
async readlink(path) {
|
|
1242
|
+
return this.unsupported("readlink", path);
|
|
1243
|
+
}
|
|
1244
|
+
async symlink(_target, path) {
|
|
1245
|
+
this.unsupported("symlink", path);
|
|
1246
|
+
}
|
|
1247
|
+
async link(_existingPath, path) {
|
|
1248
|
+
this.unsupported("link", path);
|
|
1249
|
+
}
|
|
1250
|
+
async chmod(path, _mode) {
|
|
1251
|
+
this.unsupported("chmod", path);
|
|
1252
|
+
}
|
|
1253
|
+
async utimes(input, atimeMs, mtimeMs, options = {}) {
|
|
1254
|
+
const path = this.path(input);
|
|
1255
|
+
this.writable(path);
|
|
1256
|
+
if (!Number.isFinite(atimeMs) || !Number.isFinite(mtimeMs)) fail("EINVAL", "utimes", path);
|
|
1257
|
+
const info = await this.lookup(path, options);
|
|
1258
|
+
this.requireDirectorySuffix(input, info);
|
|
1259
|
+
if (!info) fail("ENOENT", "utimes", path);
|
|
1260
|
+
if (!this.capabilities.timestamps) this.unsupported("conditional metadata mutation", path);
|
|
1261
|
+
const key = info.stat.type === "directory" ? this.directoryKey(path) : this.key(path);
|
|
1262
|
+
if (!key) this.unsupported("timestamps on an unprefixed bucket root", path);
|
|
1263
|
+
const metadata = {
|
|
1264
|
+
...info.metadata?.Metadata,
|
|
1265
|
+
"virtual-bash-atime": String(atimeMs),
|
|
1266
|
+
"virtual-bash-mtime": String(mtimeMs)
|
|
1267
|
+
};
|
|
1268
|
+
if (!info.metadata) {
|
|
1269
|
+
if (!this.transport.capabilities?.conditionalPut) this.unsupported("conditional directory marker creation", path);
|
|
1270
|
+
await this.call("utimes", path, options, () => this.transport.putObject({
|
|
1271
|
+
Bucket: this.bucket,
|
|
1272
|
+
Key: key,
|
|
1273
|
+
Body: new Uint8Array(),
|
|
1274
|
+
Metadata: metadata,
|
|
1275
|
+
IfNoneMatch: "*"
|
|
1276
|
+
}, this.requestOptions(options)));
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
if (!this.transport.capabilities?.conditionalCopy) {
|
|
1280
|
+
const current = await this.call("getObject", path, options, () => this.transport.getObject({
|
|
1281
|
+
Bucket: this.bucket,
|
|
1282
|
+
Key: key
|
|
1283
|
+
}, this.requestOptions(options)));
|
|
1284
|
+
const body = await this.body(current, path, options);
|
|
1285
|
+
await this.call("utimes", path, options, () => this.transport.putObject({
|
|
1286
|
+
Bucket: this.bucket,
|
|
1287
|
+
Key: key,
|
|
1288
|
+
Body: body,
|
|
1289
|
+
IfMatch: this.etag(current, path),
|
|
1290
|
+
Metadata: { ...current.Metadata, "virtual-bash-atime": String(atimeMs), "virtual-bash-mtime": String(mtimeMs) }
|
|
1291
|
+
}, this.requestOptions(options)));
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (info.stat.size > 5e9) this.unsupported("multipart metadata copy", path);
|
|
1295
|
+
const etag = this.etag(info.metadata, path);
|
|
1296
|
+
const result = await this.call("utimes", path, options, () => this.transport.copyObject({
|
|
1297
|
+
Bucket: this.bucket,
|
|
1298
|
+
Key: key,
|
|
1299
|
+
CopySource: encodeCopySource(this.bucket, key),
|
|
1300
|
+
CopySourceIfMatch: etag,
|
|
1301
|
+
IfMatch: etag,
|
|
1302
|
+
MetadataDirective: "REPLACE",
|
|
1303
|
+
Metadata: metadata
|
|
1304
|
+
}, this.requestOptions(options)));
|
|
1305
|
+
if (!result.CopyObjectResult?.ETag) fail("EIO", "utimes", path, "transport did not confirm a completed metadata copy");
|
|
1306
|
+
}
|
|
1307
|
+
async truncate(input, length = 0, options = {}) {
|
|
1308
|
+
const path = this.path(input);
|
|
1309
|
+
this.writable(path);
|
|
1310
|
+
validateLimit(length, "length", 0);
|
|
1311
|
+
const info = await this.lookup(path, options);
|
|
1312
|
+
this.requireDirectorySuffix(input, info);
|
|
1313
|
+
if (!info) fail("ENOENT", "truncate", path);
|
|
1314
|
+
if (info.stat.type === "directory") fail("EISDIR", "truncate", path);
|
|
1315
|
+
if (!this.transport.capabilities?.conditionalPut) this.unsupported("conditional truncate", path);
|
|
1316
|
+
if (length > this.maxReadBytes) fail("EFBIG", "truncate", path);
|
|
1317
|
+
let current = info.metadata;
|
|
1318
|
+
const body = new Uint8Array(length);
|
|
1319
|
+
if (length !== 0 && info.stat.size !== 0) {
|
|
1320
|
+
if (this.readStream) {
|
|
1321
|
+
const previous = await this.call("truncate", path, options, () => collectBytes(this.readStream(input, { ...options, endExclusive: Math.min(length, info.stat.size) }), { ...options, maxBytes: length }));
|
|
1322
|
+
body.set(previous);
|
|
1323
|
+
} else {
|
|
1324
|
+
const output = await this.get(path, options);
|
|
1325
|
+
const previous = await this.body(output, path, options);
|
|
1326
|
+
current = output;
|
|
1327
|
+
body.set(previous.subarray(0, length));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const metadata = { ...current.Metadata };
|
|
1331
|
+
delete metadata["virtual-bash-mtime"];
|
|
1332
|
+
await this.call("truncate", path, options, () => this.transport.putObject({
|
|
1333
|
+
Bucket: this.bucket,
|
|
1334
|
+
Key: this.key(path),
|
|
1335
|
+
Body: body,
|
|
1336
|
+
IfMatch: this.etag(current, path),
|
|
1337
|
+
Metadata: metadata
|
|
1338
|
+
}, this.requestOptions(options)));
|
|
1339
|
+
}
|
|
1340
|
+
async *streamRead(input, options = {}) {
|
|
1341
|
+
const path = this.path(input);
|
|
1342
|
+
const start = validateLimit(options.start ?? 0, "start", 0);
|
|
1343
|
+
const end = options.endExclusive === void 0 ? void 0 : validateLimit(options.endExclusive, "endExclusive", start);
|
|
1344
|
+
const chunkSize = validateLimit(options.chunkSize ?? 64 * 1024, "chunkSize", 1);
|
|
1345
|
+
const info = await this.lookup(path, options);
|
|
1346
|
+
this.requireDirectorySuffix(input, info);
|
|
1347
|
+
if (!info) fail("ENOENT", "readStream", path);
|
|
1348
|
+
if (info.stat.type === "directory") fail("EISDIR", "readStream", path);
|
|
1349
|
+
const stop = Math.min(end ?? info.stat.size, info.stat.size);
|
|
1350
|
+
const expected = Math.max(0, stop - start);
|
|
1351
|
+
if (expected > this.maxStreamBytes) fail("EFBIG", "readStream", path);
|
|
1352
|
+
if (expected === 0 && info.stat.size !== 0) return;
|
|
1353
|
+
let count = 0;
|
|
1354
|
+
let finished = false;
|
|
1355
|
+
const controller = new AbortController();
|
|
1356
|
+
const signalScope = options.signal ? composeAbortSignals([options.signal, controller.signal]) : void 0;
|
|
1357
|
+
const signal = signalScope?.signal ?? controller.signal;
|
|
1358
|
+
let output;
|
|
1359
|
+
let iterator;
|
|
1360
|
+
try {
|
|
1361
|
+
output = await this.call("getObject", path, options, () => this.transport.getObjectStream({
|
|
1362
|
+
Bucket: this.bucket,
|
|
1363
|
+
Key: this.key(path),
|
|
1364
|
+
IfMatch: this.etag(info.metadata, path),
|
|
1365
|
+
...expected === 0 || start === 0 && stop === info.stat.size ? {} : { Range: `bytes=${start}-${stop - 1}` }
|
|
1366
|
+
}, { abortSignal: signal }));
|
|
1367
|
+
if (output.ETag !== void 0 && output.ETag !== info.metadata?.ETag) fail("EIO", "readStream", path, "response ETag differs from requested snapshot");
|
|
1368
|
+
if (output.ContentLength !== void 0 && output.ContentLength !== expected) fail("EIO", "readStream", path, "response length differs from requested snapshot");
|
|
1369
|
+
iterator = readBytes(output.Body, signal)[Symbol.asyncIterator]();
|
|
1370
|
+
while (true) {
|
|
1371
|
+
const result = await iterator.next();
|
|
1372
|
+
if (result.done) break;
|
|
1373
|
+
const chunk = result.value;
|
|
1374
|
+
if (chunk.length > expected - count) fail("EIO", "readStream", path, "response exceeds requested length");
|
|
1375
|
+
count += chunk.length;
|
|
1376
|
+
for (let offset = 0; offset < chunk.length; offset += chunkSize) {
|
|
1377
|
+
this.checkAbort(options, "readStream", path);
|
|
1378
|
+
yield new Uint8Array(chunk.subarray(offset, offset + chunkSize));
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
if (count !== expected) fail("EIO", "readStream", path, "incomplete response body");
|
|
1382
|
+
finished = true;
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
this.checkAbort(options, "readStream", path);
|
|
1385
|
+
throw translate(error, "readStream", path);
|
|
1386
|
+
} finally {
|
|
1387
|
+
controller.abort();
|
|
1388
|
+
signalScope?.dispose();
|
|
1389
|
+
if (!finished && output) {
|
|
1390
|
+
this.dispose(iterator);
|
|
1391
|
+
this.dispose(output.Body, iterator === void 0);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
dispose(body, returnIterator = true) {
|
|
1396
|
+
const disposable = body;
|
|
1397
|
+
try {
|
|
1398
|
+
const result = typeof disposable?.destroy === "function" ? disposable.destroy() : typeof disposable?.cancel === "function" ? disposable.cancel() : returnIterator ? disposable?.return?.() : void 0;
|
|
1399
|
+
void Promise.resolve(result).catch(() => {
|
|
1400
|
+
});
|
|
1401
|
+
} catch (ignoredError) {
|
|
1402
|
+
void ignoredError;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async streamWrite(input, source, options = {}) {
|
|
1406
|
+
const path = this.path(input);
|
|
1407
|
+
this.writable(path, options.mode);
|
|
1408
|
+
const flag = options.flag ?? "w";
|
|
1409
|
+
if (!["w", "wx", "a", "ax"].includes(flag)) fail("EINVAL", "writeStream", path);
|
|
1410
|
+
if (flag !== "w" && !this.transport.capabilities?.conditionalPut) this.unsupported("conditional writes", path);
|
|
1411
|
+
const info = await this.lookup(path, options);
|
|
1412
|
+
this.requireDirectorySuffix(input, info);
|
|
1413
|
+
if (info?.stat.type === "directory" || input.endsWith("/")) fail("EISDIR", "writeStream", path);
|
|
1414
|
+
const exclusive = flag === "wx" || flag === "ax";
|
|
1415
|
+
if (exclusive && info) fail("EEXIST", "writeStream", path);
|
|
1416
|
+
if (flag === "a") {
|
|
1417
|
+
const current = info ? await this.get(path, options) : void 0;
|
|
1418
|
+
const previous = current ? await this.body(current, path, options) : new Uint8Array();
|
|
1419
|
+
const limit2 = Math.min(this.maxReadBytes, this.maxStreamBytes);
|
|
1420
|
+
if (previous.length > limit2) fail("EFBIG", "writeStream", path);
|
|
1421
|
+
const bytes = await this.call("writeStream", path, options, () => collectBytes(source, { maxBytes: limit2 - previous.length, ...options }));
|
|
1422
|
+
const body2 = new Uint8Array(previous.length + bytes.length);
|
|
1423
|
+
body2.set(previous);
|
|
1424
|
+
body2.set(bytes, previous.length);
|
|
1425
|
+
const metadata = this.writeMetadata(current, options.mode);
|
|
1426
|
+
await this.call("putObject", path, options, () => this.transport.putObject({
|
|
1427
|
+
Bucket: this.bucket,
|
|
1428
|
+
Key: this.key(path),
|
|
1429
|
+
Body: body2,
|
|
1430
|
+
...current ? { IfMatch: this.etag(current, path) } : { IfNoneMatch: "*" },
|
|
1431
|
+
...metadata ? { Metadata: metadata } : {}
|
|
1432
|
+
}, this.requestOptions(options)));
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const controller = new AbortController();
|
|
1436
|
+
const signalScope = options.signal ? composeAbortSignals([options.signal, controller.signal]) : void 0;
|
|
1437
|
+
const signal = signalScope?.signal ?? controller.signal;
|
|
1438
|
+
let finished = false;
|
|
1439
|
+
const body = (async function* () {
|
|
1440
|
+
let total = 0;
|
|
1441
|
+
for await (const chunk of readBytes(source, signal)) {
|
|
1442
|
+
if (chunk.length > this.maxStreamBytes - total) fail("EFBIG", "writeStream", path);
|
|
1443
|
+
total += chunk.length;
|
|
1444
|
+
for (let offset = 0; offset < chunk.length; offset += 64 * 1024) {
|
|
1445
|
+
this.checkAbort({ signal }, "writeStream", path);
|
|
1446
|
+
yield new Uint8Array(chunk.subarray(offset, offset + 64 * 1024));
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
finished = true;
|
|
1450
|
+
}).call(this);
|
|
1451
|
+
try {
|
|
1452
|
+
const metadata = this.writeMetadata(info?.metadata, options.mode);
|
|
1453
|
+
await this.call("putObject", path, options, () => this.transport.putObjectStream({
|
|
1454
|
+
Bucket: this.bucket,
|
|
1455
|
+
Key: this.key(path),
|
|
1456
|
+
Body: body,
|
|
1457
|
+
...exclusive ? { IfNoneMatch: "*" } : {},
|
|
1458
|
+
...metadata === void 0 ? {} : { Metadata: metadata }
|
|
1459
|
+
}, { abortSignal: signal }), exclusive ? "EEXIST" : "EAGAIN");
|
|
1460
|
+
if (!finished) fail("EIO", "writeStream", path, "transport completed without consuming the request body");
|
|
1461
|
+
} finally {
|
|
1462
|
+
controller.abort();
|
|
1463
|
+
signalScope?.dispose();
|
|
1464
|
+
this.dispose(body);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
};
|
|
1468
|
+
var s3Comparison = S3FileSystem.prototype.compareEntry;
|
|
1469
|
+
|
|
1470
|
+
// packages/safe-fs/src/fs/s3/mock.ts
|
|
1471
|
+
import { createHash } from "node:crypto";
|
|
1472
|
+
function compareKeys(left, right) {
|
|
1473
|
+
return Buffer.compare(Buffer.from(left), Buffer.from(right));
|
|
1474
|
+
}
|
|
1475
|
+
var MockS3Client = class {
|
|
1476
|
+
capabilities = Object.freeze({ conditionalPut: true, conditionalCopy: true, conditionalDelete: true, streamingRead: true, streamingWrite: true });
|
|
1477
|
+
buckets = /* @__PURE__ */ new Map();
|
|
1478
|
+
cursors = /* @__PURE__ */ new Map();
|
|
1479
|
+
history = [];
|
|
1480
|
+
pageSize;
|
|
1481
|
+
now;
|
|
1482
|
+
authorize;
|
|
1483
|
+
nextToken = 0;
|
|
1484
|
+
constructor(options) {
|
|
1485
|
+
this.pageSize = options.pageSize ?? 1e3;
|
|
1486
|
+
if (!Number.isInteger(this.pageSize) || this.pageSize < 1 || this.pageSize > 1e3) {
|
|
1487
|
+
throw new RangeError("pageSize must be an integer from 1 to 1000");
|
|
1488
|
+
}
|
|
1489
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
1490
|
+
this.authorize = options.authorize;
|
|
1491
|
+
for (const bucket of options.buckets) this.buckets.set(bucket, /* @__PURE__ */ new Map());
|
|
1492
|
+
}
|
|
1493
|
+
get requests() {
|
|
1494
|
+
return structuredClone(this.history);
|
|
1495
|
+
}
|
|
1496
|
+
async begin(operation, input, options) {
|
|
1497
|
+
if (options?.abortSignal?.aborted) throw new S3ServiceError("AbortError", 499);
|
|
1498
|
+
const request = { operation, input: structuredClone(input) };
|
|
1499
|
+
this.history.push(structuredClone(request));
|
|
1500
|
+
await this.authorize?.(request);
|
|
1501
|
+
if (options?.abortSignal?.aborted) throw new S3ServiceError("AbortError", 499);
|
|
1502
|
+
if (!this.buckets.has(input.Bucket)) throw new S3ServiceError("NoSuchBucket", 404);
|
|
1503
|
+
if ("Key" in input && (input.Key.length === 0 || Buffer.byteLength(input.Key) > 1024)) {
|
|
1504
|
+
throw new S3ServiceError("InvalidArgument", 400);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
bucket(name) {
|
|
1508
|
+
const bucket = this.buckets.get(name);
|
|
1509
|
+
if (!bucket) throw new S3ServiceError("NoSuchBucket", 404);
|
|
1510
|
+
return bucket;
|
|
1511
|
+
}
|
|
1512
|
+
object(input) {
|
|
1513
|
+
const object = this.bucket(input.Bucket).get(input.Key);
|
|
1514
|
+
if (!object) throw new S3ServiceError("NoSuchKey", 404);
|
|
1515
|
+
return object;
|
|
1516
|
+
}
|
|
1517
|
+
head(object) {
|
|
1518
|
+
return {
|
|
1519
|
+
ContentLength: object.body.byteLength,
|
|
1520
|
+
ETag: object.etag,
|
|
1521
|
+
LastModified: new Date(object.modified),
|
|
1522
|
+
Metadata: { ...object.metadata }
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
store(body, metadata = {}) {
|
|
1526
|
+
const metadataBytes = Object.entries(metadata).reduce((total, [key, value]) => total + Buffer.byteLength(key) + Buffer.byteLength(value), 0);
|
|
1527
|
+
if (metadataBytes > 2048) throw new S3ServiceError("MetadataTooLarge", 400);
|
|
1528
|
+
return {
|
|
1529
|
+
body: new Uint8Array(body),
|
|
1530
|
+
etag: `"${createHash("md5").update(body).digest("hex")}"`,
|
|
1531
|
+
modified: new Date(this.now()),
|
|
1532
|
+
metadata: { ...metadata }
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
async headObject(input, options) {
|
|
1536
|
+
await this.begin("headObject", input, options);
|
|
1537
|
+
const output = this.head(this.object(input));
|
|
1538
|
+
recordMockS3Head(output, input, this.bucket(input.Bucket));
|
|
1539
|
+
return output;
|
|
1540
|
+
}
|
|
1541
|
+
async getObject(input, options) {
|
|
1542
|
+
await this.begin("getObject", input, options);
|
|
1543
|
+
const object = this.object(input);
|
|
1544
|
+
return { ...this.head(object), Body: new Uint8Array(object.body) };
|
|
1545
|
+
}
|
|
1546
|
+
async putObject(input, options) {
|
|
1547
|
+
if (!(input.Body instanceof Uint8Array)) throw new S3ServiceError("InvalidArgument", 400);
|
|
1548
|
+
input = { ...input, Body: new Uint8Array(input.Body), ...input.Metadata ? { Metadata: { ...input.Metadata } } : {} };
|
|
1549
|
+
await this.begin("putObject", input, options);
|
|
1550
|
+
const bucket = this.bucket(input.Bucket);
|
|
1551
|
+
const previous = bucket.get(input.Key);
|
|
1552
|
+
if (input.IfMatch !== void 0 && !previous) throw new S3ServiceError("NoSuchKey", 404);
|
|
1553
|
+
if (input.IfNoneMatch === "*" && previous || input.IfMatch !== void 0 && previous?.etag !== input.IfMatch) {
|
|
1554
|
+
throw new S3ServiceError("PreconditionFailed", 412);
|
|
1555
|
+
}
|
|
1556
|
+
const object = this.store(input.Body, input.Metadata);
|
|
1557
|
+
bucket.set(input.Key, object);
|
|
1558
|
+
return { ETag: object.etag };
|
|
1559
|
+
}
|
|
1560
|
+
async getObjectStream(input, options) {
|
|
1561
|
+
await this.begin("getObject", input, options);
|
|
1562
|
+
const object = this.object(input);
|
|
1563
|
+
if (input.IfMatch !== void 0 && input.IfMatch !== object.etag) throw new S3ServiceError("PreconditionFailed", 412);
|
|
1564
|
+
let start = 0;
|
|
1565
|
+
let end = object.body.length;
|
|
1566
|
+
if (input.Range !== void 0) {
|
|
1567
|
+
const range = /^bytes=(\d+)-(\d+)$/.exec(input.Range);
|
|
1568
|
+
if (!range) throw new S3ServiceError("InvalidArgument", 400);
|
|
1569
|
+
start = Number(range[1]);
|
|
1570
|
+
end = Math.min(Number(range[2]) + 1, end);
|
|
1571
|
+
if (start >= end) throw new S3ServiceError("InvalidRange", 416);
|
|
1572
|
+
}
|
|
1573
|
+
return {
|
|
1574
|
+
...this.head(object),
|
|
1575
|
+
ContentLength: end - start,
|
|
1576
|
+
Body: (async function* () {
|
|
1577
|
+
for (let offset = start; offset < end; offset += 64 * 1024) {
|
|
1578
|
+
if (options?.abortSignal?.aborted) throw new S3ServiceError("AbortError", 499);
|
|
1579
|
+
yield object.body.slice(offset, Math.min(offset + 64 * 1024, end));
|
|
1580
|
+
}
|
|
1581
|
+
})()
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
async putObjectStream(input, options) {
|
|
1585
|
+
const snapshot = { ...input, Body: new Uint8Array(), ...input.Metadata ? { Metadata: { ...input.Metadata } } : {} };
|
|
1586
|
+
await this.begin("putObject", snapshot, options);
|
|
1587
|
+
const body = await collectBytes(input.Body, { maxBytes: 5e9, ...options?.abortSignal ? { signal: options.abortSignal } : {} });
|
|
1588
|
+
const bucket = this.bucket(input.Bucket);
|
|
1589
|
+
const previous = bucket.get(input.Key);
|
|
1590
|
+
if (input.IfMatch !== void 0 && !previous) throw new S3ServiceError("NoSuchKey", 404);
|
|
1591
|
+
if (input.IfNoneMatch === "*" && previous || input.IfMatch !== void 0 && previous?.etag !== input.IfMatch) {
|
|
1592
|
+
throw new S3ServiceError("PreconditionFailed", 412);
|
|
1593
|
+
}
|
|
1594
|
+
const object = this.store(body, snapshot.Metadata);
|
|
1595
|
+
bucket.set(input.Key, object);
|
|
1596
|
+
return { ETag: object.etag };
|
|
1597
|
+
}
|
|
1598
|
+
async deleteObject(input, options) {
|
|
1599
|
+
await this.begin("deleteObject", input, options);
|
|
1600
|
+
const bucket = this.bucket(input.Bucket);
|
|
1601
|
+
const previous = bucket.get(input.Key);
|
|
1602
|
+
if (input.IfMatch !== void 0 && !previous) throw new S3ServiceError("NoSuchKey", 404);
|
|
1603
|
+
if (previous && input.IfMatch !== void 0 && input.IfMatch !== "*" && previous.etag !== input.IfMatch) {
|
|
1604
|
+
throw new S3ServiceError("PreconditionFailed", 412);
|
|
1605
|
+
}
|
|
1606
|
+
bucket.delete(input.Key);
|
|
1607
|
+
return {};
|
|
1608
|
+
}
|
|
1609
|
+
async copyObject(input, options) {
|
|
1610
|
+
await this.begin("copyObject", input, options);
|
|
1611
|
+
let decoded;
|
|
1612
|
+
try {
|
|
1613
|
+
decoded = decodeURIComponent(input.CopySource.replace(/^\//, ""));
|
|
1614
|
+
} catch {
|
|
1615
|
+
throw new S3ServiceError("InvalidArgument", 400);
|
|
1616
|
+
}
|
|
1617
|
+
const separator = decoded.indexOf("/");
|
|
1618
|
+
if (separator < 1) throw new S3ServiceError("InvalidArgument", 400);
|
|
1619
|
+
const source = this.object({ Bucket: decoded.slice(0, separator), Key: decoded.slice(separator + 1) });
|
|
1620
|
+
if (input.CopySourceIfMatch !== void 0 && input.CopySourceIfMatch !== source.etag) {
|
|
1621
|
+
throw new S3ServiceError("PreconditionFailed", 412);
|
|
1622
|
+
}
|
|
1623
|
+
const bucket = this.bucket(input.Bucket);
|
|
1624
|
+
if (input.IfNoneMatch === "*" && bucket.has(input.Key)) throw new S3ServiceError("PreconditionFailed", 412);
|
|
1625
|
+
if (input.IfMatch !== void 0 && bucket.get(input.Key)?.etag !== input.IfMatch) throw new S3ServiceError("PreconditionFailed", 412);
|
|
1626
|
+
const copy = this.store(source.body, input.MetadataDirective === "REPLACE" ? input.Metadata : source.metadata);
|
|
1627
|
+
bucket.set(input.Key, copy);
|
|
1628
|
+
return { CopyObjectResult: { ETag: copy.etag, LastModified: new Date(copy.modified) } };
|
|
1629
|
+
}
|
|
1630
|
+
async listObjectsV2(input, options) {
|
|
1631
|
+
await this.begin("listObjectsV2", input, options);
|
|
1632
|
+
const maxKeys = input.MaxKeys ?? 1e3;
|
|
1633
|
+
if (!Number.isInteger(maxKeys) || maxKeys < 0 || maxKeys > 1e3) throw new S3ServiceError("InvalidArgument", 400);
|
|
1634
|
+
const prefix = input.Prefix ?? "";
|
|
1635
|
+
const delimiter = input.Delimiter ?? "";
|
|
1636
|
+
const scope = JSON.stringify([input.Bucket, prefix, delimiter]);
|
|
1637
|
+
const cursor = input.ContinuationToken === void 0 ? void 0 : this.cursors.get(input.ContinuationToken);
|
|
1638
|
+
if (input.ContinuationToken !== void 0 && (!cursor || cursor.scope !== scope)) {
|
|
1639
|
+
throw new S3ServiceError("InvalidArgument", 400, "invalid continuation token");
|
|
1640
|
+
}
|
|
1641
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1642
|
+
for (const [key, object] of this.bucket(input.Bucket)) {
|
|
1643
|
+
if (!key.startsWith(prefix)) continue;
|
|
1644
|
+
const separator = delimiter === "" ? -1 : key.indexOf(delimiter, prefix.length);
|
|
1645
|
+
if (separator >= 0) {
|
|
1646
|
+
const commonPrefix = key.slice(0, separator + delimiter.length);
|
|
1647
|
+
entries.set(commonPrefix, commonPrefix);
|
|
1648
|
+
} else {
|
|
1649
|
+
entries.set(key, { Key: key, Size: object.body.byteLength, ETag: object.etag, LastModified: new Date(object.modified) });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const ordered = [...entries.keys()].sort(compareKeys).filter((key) => cursor === void 0 || compareKeys(key, cursor.after) > 0);
|
|
1653
|
+
const selected = ordered.slice(0, Math.min(maxKeys, this.pageSize));
|
|
1654
|
+
const contents = [];
|
|
1655
|
+
const prefixes = [];
|
|
1656
|
+
for (const key of selected) {
|
|
1657
|
+
const entry = entries.get(key);
|
|
1658
|
+
if (typeof entry === "string") prefixes.push({ Prefix: entry });
|
|
1659
|
+
else if (entry) contents.push(entry);
|
|
1660
|
+
}
|
|
1661
|
+
const truncated = maxKeys > 0 && selected.length < ordered.length;
|
|
1662
|
+
let token;
|
|
1663
|
+
if (truncated) {
|
|
1664
|
+
token = `mock-s3-cursor-${++this.nextToken}`;
|
|
1665
|
+
this.cursors.set(token, { scope, after: selected[selected.length - 1] });
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
Contents: contents,
|
|
1669
|
+
CommonPrefixes: prefixes,
|
|
1670
|
+
KeyCount: selected.length,
|
|
1671
|
+
IsTruncated: truncated,
|
|
1672
|
+
...token === void 0 ? {} : { NextContinuationToken: token }
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
// packages/safe-fs/src/fs/s3/http/transport.ts
|
|
1678
|
+
import { isIP } from "node:net";
|
|
1679
|
+
|
|
1680
|
+
// packages/safe-fs/src/fs/s3/http/request.ts
|
|
1681
|
+
import { once } from "node:events";
|
|
1682
|
+
import { request as httpRequest } from "node:http";
|
|
1683
|
+
import { request as httpsRequest } from "node:https";
|
|
1684
|
+
function scopeFor(signal, timeout) {
|
|
1685
|
+
const controller = new AbortController();
|
|
1686
|
+
const abort = () => controller.abort(signal?.reason);
|
|
1687
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
1688
|
+
if (signal?.aborted) abort();
|
|
1689
|
+
const timer = setTimeout(() => controller.abort(new S3ServiceError("RequestTimeout", 408)), timeout);
|
|
1690
|
+
return {
|
|
1691
|
+
signal: controller.signal,
|
|
1692
|
+
finish: () => {
|
|
1693
|
+
clearTimeout(timer);
|
|
1694
|
+
signal?.removeEventListener("abort", abort);
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
async function abortable(pending, signal) {
|
|
1699
|
+
let abort;
|
|
1700
|
+
try {
|
|
1701
|
+
return await new Promise((resolve2, reject) => {
|
|
1702
|
+
abort = () => reject(signal.reason);
|
|
1703
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1704
|
+
pending.then(resolve2, reject);
|
|
1705
|
+
if (signal.aborted) abort();
|
|
1706
|
+
});
|
|
1707
|
+
} finally {
|
|
1708
|
+
if (abort) signal.removeEventListener("abort", abort);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
async function sendRequest(options, body, scope, factory) {
|
|
1712
|
+
const signal = scope.signal;
|
|
1713
|
+
signal.throwIfAborted();
|
|
1714
|
+
return new Promise((resolve2, reject) => {
|
|
1715
|
+
let request;
|
|
1716
|
+
let response;
|
|
1717
|
+
let headersReceived = false;
|
|
1718
|
+
let finished = false;
|
|
1719
|
+
const cleanup = () => {
|
|
1720
|
+
signal.removeEventListener("abort", abort);
|
|
1721
|
+
scope.finish();
|
|
1722
|
+
};
|
|
1723
|
+
const close = () => {
|
|
1724
|
+
if (finished) return;
|
|
1725
|
+
finished = true;
|
|
1726
|
+
cleanup();
|
|
1727
|
+
response?.destroy();
|
|
1728
|
+
request?.destroy();
|
|
1729
|
+
};
|
|
1730
|
+
const fail2 = (error) => {
|
|
1731
|
+
reject(signal.aborted ? signal.reason : error);
|
|
1732
|
+
close();
|
|
1733
|
+
};
|
|
1734
|
+
const abort = () => fail2(signal.reason);
|
|
1735
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1736
|
+
try {
|
|
1737
|
+
const create = factory ?? (options.protocol === "https:" ? httpsRequest : httpRequest);
|
|
1738
|
+
request = create(options, (message) => {
|
|
1739
|
+
response = message;
|
|
1740
|
+
message.on("error", () => {
|
|
1741
|
+
});
|
|
1742
|
+
if (finished || signal.aborted) {
|
|
1743
|
+
close();
|
|
1744
|
+
message.destroy();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
headersReceived = true;
|
|
1748
|
+
message.once("close", cleanup);
|
|
1749
|
+
let claimed = false;
|
|
1750
|
+
const source = {
|
|
1751
|
+
[Symbol.asyncIterator]() {
|
|
1752
|
+
if (claimed) throw new S3ServiceError("InvalidResponse", 502, "response body is single-use");
|
|
1753
|
+
claimed = true;
|
|
1754
|
+
const iterator = message[Symbol.asyncIterator]();
|
|
1755
|
+
let ended = false;
|
|
1756
|
+
return {
|
|
1757
|
+
async next() {
|
|
1758
|
+
try {
|
|
1759
|
+
signal.throwIfAborted();
|
|
1760
|
+
if (ended) return { done: true, value: void 0 };
|
|
1761
|
+
const result = await abortable(iterator.next(), signal);
|
|
1762
|
+
signal.throwIfAborted();
|
|
1763
|
+
if (result.done) {
|
|
1764
|
+
ended = true;
|
|
1765
|
+
if (!message.complete) throw new S3ServiceError("InvalidResponse", 502, "incomplete HTTP response");
|
|
1766
|
+
close();
|
|
1767
|
+
return { done: true, value: void 0 };
|
|
1768
|
+
}
|
|
1769
|
+
if (!(result.value instanceof Uint8Array)) throw new S3ServiceError("InvalidResponse", 502, "nonbinary HTTP body");
|
|
1770
|
+
return { done: false, value: new Uint8Array(result.value) };
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
ended = true;
|
|
1773
|
+
close();
|
|
1774
|
+
throw signal.aborted ? signal.reason : error;
|
|
1775
|
+
}
|
|
1776
|
+
},
|
|
1777
|
+
async return() {
|
|
1778
|
+
ended = true;
|
|
1779
|
+
close();
|
|
1780
|
+
return { done: true, value: void 0 };
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
resolve2({ message, body: source, close });
|
|
1786
|
+
});
|
|
1787
|
+
request.on("error", fail2);
|
|
1788
|
+
request.once("close", () => {
|
|
1789
|
+
if (!headersReceived) fail2(new S3ServiceError("InvalidResponse", 502, "connection closed before response headers"));
|
|
1790
|
+
});
|
|
1791
|
+
if (signal.aborted || finished) {
|
|
1792
|
+
abort();
|
|
1793
|
+
request.destroy();
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
void (async () => {
|
|
1797
|
+
for (let offset = 0; offset < body.length; offset += 64 * 1024) {
|
|
1798
|
+
signal.throwIfAborted();
|
|
1799
|
+
if (!request.write(body.subarray(offset, offset + 64 * 1024))) await once(request, "drain", { signal });
|
|
1800
|
+
}
|
|
1801
|
+
signal.throwIfAborted();
|
|
1802
|
+
request.end();
|
|
1803
|
+
})().catch(fail2);
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
fail2(error);
|
|
1806
|
+
}
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
function limitedBody(response, maximum, expected) {
|
|
1810
|
+
if (expected !== void 0 && expected > maximum) {
|
|
1811
|
+
response.close();
|
|
1812
|
+
throw new S3ServiceError("EntityTooLarge", 413, "response exceeds byte limit");
|
|
1813
|
+
}
|
|
1814
|
+
let claimed = false;
|
|
1815
|
+
return {
|
|
1816
|
+
[Symbol.asyncIterator]() {
|
|
1817
|
+
if (claimed) throw new S3ServiceError("InvalidResponse", 502, "response body is single-use");
|
|
1818
|
+
claimed = true;
|
|
1819
|
+
const iterator = response.body[Symbol.asyncIterator]();
|
|
1820
|
+
let count = 0;
|
|
1821
|
+
return {
|
|
1822
|
+
async next() {
|
|
1823
|
+
try {
|
|
1824
|
+
const result = await iterator.next();
|
|
1825
|
+
if (result.done) {
|
|
1826
|
+
if (expected !== void 0 && count !== expected) throw new S3ServiceError("InvalidResponse", 502, "response content length mismatch");
|
|
1827
|
+
} else {
|
|
1828
|
+
count += result.value.length;
|
|
1829
|
+
if (count > maximum) throw new S3ServiceError("EntityTooLarge", 413, "response exceeds byte limit");
|
|
1830
|
+
if (expected !== void 0 && count > expected) throw new S3ServiceError("InvalidResponse", 502, "response content length mismatch");
|
|
1831
|
+
}
|
|
1832
|
+
return result;
|
|
1833
|
+
} catch (error) {
|
|
1834
|
+
response.close();
|
|
1835
|
+
throw error;
|
|
1836
|
+
}
|
|
1837
|
+
},
|
|
1838
|
+
async return() {
|
|
1839
|
+
response.close();
|
|
1840
|
+
await iterator.return?.();
|
|
1841
|
+
return { done: true, value: void 0 };
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
async function collect(response, maximum, expected) {
|
|
1848
|
+
const chunks = [];
|
|
1849
|
+
let count = 0;
|
|
1850
|
+
try {
|
|
1851
|
+
for await (const chunk of limitedBody(response, maximum, expected)) {
|
|
1852
|
+
chunks.push(chunk);
|
|
1853
|
+
count += chunk.length;
|
|
1854
|
+
}
|
|
1855
|
+
return Buffer.concat(chunks, count);
|
|
1856
|
+
} finally {
|
|
1857
|
+
response.close();
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// packages/safe-fs/src/fs/s3/http/signature.ts
|
|
1862
|
+
import { createHash as createHash2, createHmac } from "node:crypto";
|
|
1863
|
+
function invalid(message) {
|
|
1864
|
+
throw new S3ServiceError("InvalidArgument", 400, message);
|
|
1865
|
+
}
|
|
1866
|
+
function uriEncode(value, preserveSlash = false) {
|
|
1867
|
+
if (typeof value !== "string") invalid("expected a UTF-8 string");
|
|
1868
|
+
try {
|
|
1869
|
+
const encoded = encodeURIComponent(value).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1870
|
+
return preserveSlash ? encoded.replace(/%2F/g, "/") : encoded;
|
|
1871
|
+
} catch {
|
|
1872
|
+
return invalid("invalid Unicode string");
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function canonicalQuery(entries) {
|
|
1876
|
+
return entries.map(([name, value]) => [uriEncode(name), uriEncode(value)]).sort(([leftName, leftValue], [rightName, rightValue]) => leftName < rightName ? -1 : leftName > rightName ? 1 : leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0).map(([name, value]) => `${name}=${value}`).join("&");
|
|
1877
|
+
}
|
|
1878
|
+
function sha256(bytes) {
|
|
1879
|
+
return createHash2("sha256").update(bytes).digest("hex");
|
|
1880
|
+
}
|
|
1881
|
+
function headerValue(value) {
|
|
1882
|
+
if (typeof value !== "string" || /[^\x20-\x7e\t]/.test(value)) invalid("header values must be printable ASCII without line breaks");
|
|
1883
|
+
return value.trim().replace(/[ \t]+/g, " ");
|
|
1884
|
+
}
|
|
1885
|
+
function signRequest(input) {
|
|
1886
|
+
const credentials = input.credentials;
|
|
1887
|
+
if (!credentials || typeof credentials.accessKeyId !== "string" || !/^[A-Za-z0-9_-]+$/.test(credentials.accessKeyId) || typeof credentials.secretAccessKey !== "string" || credentials.secretAccessKey.length === 0 || credentials.secretAccessKey.length > 4096) invalid("explicit signing credentials are required");
|
|
1888
|
+
if (!/^[a-z0-9-]{1,64}$/.test(input.region)) invalid("invalid signing region");
|
|
1889
|
+
if (!(input.date instanceof Date) || !Number.isFinite(input.date.getTime()) || input.date.getUTCFullYear() < 1970 || input.date.getUTCFullYear() > 9999) invalid("invalid signing clock");
|
|
1890
|
+
const timestamp2 = input.date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
1891
|
+
const date = timestamp2.slice(0, 8);
|
|
1892
|
+
const headers = /* @__PURE__ */ Object.create(null);
|
|
1893
|
+
for (const [name, value] of Object.entries(input.headers)) {
|
|
1894
|
+
const lower = name.toLowerCase();
|
|
1895
|
+
if (!/^[a-z0-9-]+$/.test(lower) || lower in headers) invalid("invalid or duplicate signing header");
|
|
1896
|
+
headers[lower] = headerValue(value);
|
|
1897
|
+
}
|
|
1898
|
+
if (!headers.host) invalid("host is required for signing");
|
|
1899
|
+
const payloadHash = sha256(input.body);
|
|
1900
|
+
headers["x-amz-date"] = timestamp2;
|
|
1901
|
+
headers["x-amz-content-sha256"] = payloadHash;
|
|
1902
|
+
if (credentials.sessionToken !== void 0) {
|
|
1903
|
+
headers["x-amz-security-token"] = headerValue(credentials.sessionToken);
|
|
1904
|
+
if (!headers["x-amz-security-token"]) invalid("session token must not be empty");
|
|
1905
|
+
}
|
|
1906
|
+
const names = Object.keys(headers).sort();
|
|
1907
|
+
const signedHeaders = names.join(";");
|
|
1908
|
+
const canonicalRequest = [
|
|
1909
|
+
input.method,
|
|
1910
|
+
input.path,
|
|
1911
|
+
input.query,
|
|
1912
|
+
names.map((name) => `${name}:${headers[name]}
|
|
1913
|
+
`).join(""),
|
|
1914
|
+
signedHeaders,
|
|
1915
|
+
payloadHash
|
|
1916
|
+
].join("\n");
|
|
1917
|
+
const scope = `${date}/${input.region}/s3/aws4_request`;
|
|
1918
|
+
const stringToSign = ["AWS4-HMAC-SHA256", timestamp2, scope, sha256(canonicalRequest)].join("\n");
|
|
1919
|
+
let key = createHmac("sha256", `AWS4${credentials.secretAccessKey}`).update(date).digest();
|
|
1920
|
+
for (const part of [input.region, "s3", "aws4_request"]) key = createHmac("sha256", key).update(part).digest();
|
|
1921
|
+
const signature = createHmac("sha256", key).update(stringToSign).digest("hex");
|
|
1922
|
+
headers.authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
1923
|
+
if (Object.entries(headers).reduce((size, [name, value]) => size + name.length + value.length + 4, 0) > 16 * 1024) invalid("request headers exceed 16 KiB");
|
|
1924
|
+
return { headers, canonicalRequest, signature };
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// packages/safe-fs/src/fs/s3/http/xml.ts
|
|
1928
|
+
function malformed(message = "invalid S3 XML response") {
|
|
1929
|
+
throw new S3ServiceError("InvalidResponse", 502, message);
|
|
1930
|
+
}
|
|
1931
|
+
function validCharacter(code) {
|
|
1932
|
+
return code === 9 || code === 10 || code === 13 || code >= 32 && code <= 55295 || code >= 57344 && code <= 65533 || code >= 65536 && code <= 1114111;
|
|
1933
|
+
}
|
|
1934
|
+
function decodeEntities(text2) {
|
|
1935
|
+
let result = "";
|
|
1936
|
+
let position = 0;
|
|
1937
|
+
while (position < text2.length) {
|
|
1938
|
+
const start = text2.indexOf("&", position);
|
|
1939
|
+
if (start < 0) return result + text2.slice(position);
|
|
1940
|
+
result += text2.slice(position, start);
|
|
1941
|
+
const end = text2.indexOf(";", start + 1);
|
|
1942
|
+
if (end < 0 || end - start > 12) malformed("invalid XML entity");
|
|
1943
|
+
const entity = text2.slice(start + 1, end);
|
|
1944
|
+
const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
1945
|
+
if (Object.hasOwn(named, entity)) result += named[entity];
|
|
1946
|
+
else {
|
|
1947
|
+
const hexadecimal = entity.startsWith("#x");
|
|
1948
|
+
const digits = entity.slice(hexadecimal ? 2 : 1);
|
|
1949
|
+
if (!entity.startsWith("#") || !(hexadecimal ? /^[0-9a-fA-F]+$/ : /^[0-9]+$/).test(digits)) malformed("unknown XML entity");
|
|
1950
|
+
const code = Number.parseInt(digits, hexadecimal ? 16 : 10);
|
|
1951
|
+
if (!validCharacter(code)) malformed("invalid XML code point");
|
|
1952
|
+
result += String.fromCodePoint(code);
|
|
1953
|
+
}
|
|
1954
|
+
position = end + 1;
|
|
1955
|
+
}
|
|
1956
|
+
return result;
|
|
1957
|
+
}
|
|
1958
|
+
function parseXml(bytes) {
|
|
1959
|
+
let xml;
|
|
1960
|
+
try {
|
|
1961
|
+
xml = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1962
|
+
} catch {
|
|
1963
|
+
return malformed("XML is not UTF-8");
|
|
1964
|
+
}
|
|
1965
|
+
for (const character of xml) if (!validCharacter(character.codePointAt(0))) malformed("invalid XML character");
|
|
1966
|
+
xml = xml.replace(/\r\n?/g, "\n");
|
|
1967
|
+
let position = 0;
|
|
1968
|
+
let nodes = 0;
|
|
1969
|
+
let root;
|
|
1970
|
+
const stack = [];
|
|
1971
|
+
const whitespace = () => {
|
|
1972
|
+
while (position < xml.length && /[ \t\r\n]/.test(xml[position])) position++;
|
|
1973
|
+
};
|
|
1974
|
+
const name = () => {
|
|
1975
|
+
const start = position;
|
|
1976
|
+
if (!/[A-Za-z_]/.test(xml[position] ?? "")) malformed("invalid XML name");
|
|
1977
|
+
while (position < xml.length && /[A-Za-z0-9_.:-]/.test(xml[position])) position++;
|
|
1978
|
+
const result = xml.slice(start, position);
|
|
1979
|
+
if (result.length > 256) malformed("XML name too long");
|
|
1980
|
+
return result;
|
|
1981
|
+
};
|
|
1982
|
+
while (position < xml.length) {
|
|
1983
|
+
if (xml[position] !== "<") {
|
|
1984
|
+
const end = xml.indexOf("<", position);
|
|
1985
|
+
const text2 = xml.slice(position, end < 0 ? xml.length : end);
|
|
1986
|
+
if (text2.includes("]]>")) malformed();
|
|
1987
|
+
const current = stack.at(-1);
|
|
1988
|
+
if (current) current.text += decodeEntities(text2);
|
|
1989
|
+
else if (text2.trim()) malformed("text outside XML root");
|
|
1990
|
+
position = end < 0 ? xml.length : end;
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
if (xml.startsWith("<?xml ", position) && !root && position === 0) {
|
|
1994
|
+
const end = xml.indexOf("?>", position);
|
|
1995
|
+
if (end < 0 || end > 256) malformed();
|
|
1996
|
+
const declaration = xml.slice(position, end);
|
|
1997
|
+
if (!/^<\?xml\s+version\s*=\s*(?:"1\.0"|'1\.0')(?:\s+encoding\s*=\s*(?:"[Uu][Tt][Ff]-8"|'[Uu][Tt][Ff]-8'))?(?:\s+standalone\s*=\s*(?:"(?:yes|no)"|'(?:yes|no)'))?\s*$/.test(declaration)) malformed("unsupported XML declaration");
|
|
1998
|
+
position = end + 2;
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
if (xml.startsWith("<!--", position)) {
|
|
2002
|
+
const end = xml.indexOf("-->", position + 4);
|
|
2003
|
+
const comment = xml.slice(position + 4, end);
|
|
2004
|
+
if (end < 0 || comment.includes("--") || comment.endsWith("-")) malformed();
|
|
2005
|
+
position = end + 3;
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
if (xml.startsWith("<![CDATA[", position)) {
|
|
2009
|
+
const end = xml.indexOf("]]>", position + 9);
|
|
2010
|
+
const current = stack.at(-1);
|
|
2011
|
+
if (end < 0 || !current) malformed();
|
|
2012
|
+
current.text += xml.slice(position + 9, end);
|
|
2013
|
+
position = end + 3;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
position++;
|
|
2017
|
+
const closing = xml[position] === "/";
|
|
2018
|
+
if (closing) position++;
|
|
2019
|
+
const tag = name();
|
|
2020
|
+
if (closing) {
|
|
2021
|
+
whitespace();
|
|
2022
|
+
if (xml[position++] !== ">" || stack.pop()?.name !== tag) malformed("mismatched XML close tag");
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
const attributes = /* @__PURE__ */ new Set();
|
|
2026
|
+
while (true) {
|
|
2027
|
+
const previous = position;
|
|
2028
|
+
whitespace();
|
|
2029
|
+
if (xml[position] === ">" || xml[position] === "/") break;
|
|
2030
|
+
if (previous === position || attributes.size >= 32) malformed("invalid XML attributes");
|
|
2031
|
+
const attribute = name();
|
|
2032
|
+
if (attributes.has(attribute)) malformed("duplicate XML attribute");
|
|
2033
|
+
attributes.add(attribute);
|
|
2034
|
+
whitespace();
|
|
2035
|
+
if (xml[position++] !== "=") malformed();
|
|
2036
|
+
whitespace();
|
|
2037
|
+
const quote = xml[position++];
|
|
2038
|
+
if (quote !== '"' && quote !== "'") malformed();
|
|
2039
|
+
const end = xml.indexOf(quote, position);
|
|
2040
|
+
if (end < 0 || xml.slice(position, end).includes("<")) malformed();
|
|
2041
|
+
decodeEntities(xml.slice(position, end));
|
|
2042
|
+
position = end + 1;
|
|
2043
|
+
}
|
|
2044
|
+
const selfClosing = xml[position] === "/";
|
|
2045
|
+
if (selfClosing) position++;
|
|
2046
|
+
if (xml[position++] !== ">") malformed();
|
|
2047
|
+
if (++nodes > 32768 || stack.length >= 32) malformed("XML structure limit exceeded");
|
|
2048
|
+
const node = { name: tag, text: "", children: [] };
|
|
2049
|
+
const parent = stack.at(-1);
|
|
2050
|
+
if (parent) parent.children.push(node);
|
|
2051
|
+
else if (root) malformed("multiple XML roots");
|
|
2052
|
+
else root = node;
|
|
2053
|
+
if (!selfClosing) stack.push(node);
|
|
2054
|
+
}
|
|
2055
|
+
if (!root || stack.length) malformed("incomplete XML");
|
|
2056
|
+
return root;
|
|
2057
|
+
}
|
|
2058
|
+
function children(node, name) {
|
|
2059
|
+
return node.children.filter((child) => child.name === name);
|
|
2060
|
+
}
|
|
2061
|
+
function text(node, name, required = false) {
|
|
2062
|
+
const matches = children(node, name);
|
|
2063
|
+
if (matches.length > 1 || required && matches.length !== 1) malformed(`invalid ${name} count`);
|
|
2064
|
+
const child = matches[0];
|
|
2065
|
+
if (child?.children.length) malformed(`invalid ${name} value`);
|
|
2066
|
+
return child?.text;
|
|
2067
|
+
}
|
|
2068
|
+
function integer2(value) {
|
|
2069
|
+
if (!/^[0-9]+$/.test(value) || !Number.isSafeInteger(Number(value))) malformed("invalid integer");
|
|
2070
|
+
return Number(value);
|
|
2071
|
+
}
|
|
2072
|
+
function timestamp(value) {
|
|
2073
|
+
const date = new Date(value);
|
|
2074
|
+
if (!Number.isFinite(date.getTime())) malformed("invalid timestamp");
|
|
2075
|
+
return date;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
// packages/safe-fs/src/fs/s3/http/transport.ts
|
|
2079
|
+
var empty = new Uint8Array();
|
|
2080
|
+
function limit(value, fallback, name, maximum = 1024 * 1024 * 1024) {
|
|
2081
|
+
const result = value ?? fallback;
|
|
2082
|
+
if (!Number.isSafeInteger(result) || result < 1 || result > maximum) invalid(`invalid ${name}`);
|
|
2083
|
+
return result;
|
|
2084
|
+
}
|
|
2085
|
+
function boolean(value, fallback, name) {
|
|
2086
|
+
if (value !== void 0 && typeof value !== "boolean") invalid(`invalid ${name}`);
|
|
2087
|
+
return value ?? fallback;
|
|
2088
|
+
}
|
|
2089
|
+
function bucketName(bucket) {
|
|
2090
|
+
if (typeof bucket !== "string" || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..") || bucket.includes(".-") || bucket.includes("-.") || isIP(bucket)) invalid("expected a standard S3 bucket name");
|
|
2091
|
+
}
|
|
2092
|
+
function objectKey(key) {
|
|
2093
|
+
if (typeof key !== "string" || !key || Buffer.byteLength(key) > 1024) invalid("object keys must contain 1..1024 UTF-8 bytes");
|
|
2094
|
+
uriEncode(key);
|
|
2095
|
+
}
|
|
2096
|
+
function header(message, name) {
|
|
2097
|
+
const values = message.headersDistinct[name];
|
|
2098
|
+
if (values && values.length !== 1) malformed(`duplicate ${name} header`);
|
|
2099
|
+
return values?.[0];
|
|
2100
|
+
}
|
|
2101
|
+
function head(message) {
|
|
2102
|
+
const length = header(message, "content-length");
|
|
2103
|
+
const modified = header(message, "last-modified");
|
|
2104
|
+
const etag = header(message, "etag");
|
|
2105
|
+
const metadata = /* @__PURE__ */ Object.create(null);
|
|
2106
|
+
for (const name of Object.keys(message.headersDistinct)) {
|
|
2107
|
+
if (name.startsWith("x-amz-meta-")) metadata[name.slice(11)] = header(message, name);
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
...length === void 0 ? {} : { ContentLength: integer2(length) },
|
|
2111
|
+
...modified === void 0 ? {} : { LastModified: timestamp(modified) },
|
|
2112
|
+
...etag === void 0 ? {} : { ETag: etag },
|
|
2113
|
+
Metadata: metadata
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
function metadataHeaders(metadata) {
|
|
2117
|
+
const headers = /* @__PURE__ */ Object.create(null);
|
|
2118
|
+
let size = 0;
|
|
2119
|
+
for (const [name, value] of Object.entries(metadata ?? {})) {
|
|
2120
|
+
const lower = name.toLowerCase();
|
|
2121
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/.test(lower) || `x-amz-meta-${lower}` in headers) invalid("invalid or duplicate metadata key");
|
|
2122
|
+
const normalized = headerValue(value);
|
|
2123
|
+
size += Buffer.byteLength(lower) + Buffer.byteLength(normalized);
|
|
2124
|
+
if (size > 2048) invalid("metadata exceeds 2 KiB");
|
|
2125
|
+
headers[`x-amz-meta-${lower}`] = normalized;
|
|
2126
|
+
}
|
|
2127
|
+
return headers;
|
|
2128
|
+
}
|
|
2129
|
+
function conditions(input) {
|
|
2130
|
+
const headers = {};
|
|
2131
|
+
if (input.IfMatch !== void 0) {
|
|
2132
|
+
const value = headerValue(input.IfMatch);
|
|
2133
|
+
if (!value) invalid("empty IfMatch");
|
|
2134
|
+
headers["if-match"] = value;
|
|
2135
|
+
}
|
|
2136
|
+
if (input.IfNoneMatch !== void 0) {
|
|
2137
|
+
if (input.IfNoneMatch !== "*") invalid("IfNoneMatch must be '*'");
|
|
2138
|
+
headers["if-none-match"] = "*";
|
|
2139
|
+
}
|
|
2140
|
+
return headers;
|
|
2141
|
+
}
|
|
2142
|
+
function requireCondition(enabled, present) {
|
|
2143
|
+
if (present && !enabled) throw new S3ServiceError("NotImplemented", 501, "conditional operation requires independently verified provider enforcement");
|
|
2144
|
+
}
|
|
2145
|
+
function copySource(value) {
|
|
2146
|
+
if (typeof value !== "string" || value.length > 4096 || !/^\/?[A-Za-z0-9_.~!$&'()*+,;=:@%/-]+$/.test(value)) invalid("CopySource must already be URI encoded without a query");
|
|
2147
|
+
const source = value.startsWith("/") ? value.slice(1) : value;
|
|
2148
|
+
const slash = source.indexOf("/");
|
|
2149
|
+
if (slash < 1) invalid("CopySource requires bucket/key");
|
|
2150
|
+
let bucket;
|
|
2151
|
+
let key;
|
|
2152
|
+
try {
|
|
2153
|
+
bucket = decodeURIComponent(source.slice(0, slash));
|
|
2154
|
+
key = decodeURIComponent(source.slice(slash + 1));
|
|
2155
|
+
} catch {
|
|
2156
|
+
return invalid("invalid CopySource encoding");
|
|
2157
|
+
}
|
|
2158
|
+
bucketName(bucket);
|
|
2159
|
+
objectKey(key);
|
|
2160
|
+
return value;
|
|
2161
|
+
}
|
|
2162
|
+
function rangeHeader(value) {
|
|
2163
|
+
if (value === void 0) return void 0;
|
|
2164
|
+
if (typeof value !== "string" || !/^bytes=(?:[0-9]+-[0-9]*|-[0-9]+)$/.test(value)) invalid("unsupported byte range");
|
|
2165
|
+
const [start, end] = value.slice(6).split("-");
|
|
2166
|
+
if (start && !Number.isSafeInteger(Number(start)) || end && !Number.isSafeInteger(Number(end)) || !start && Number(end) === 0 || start && end && Number(start) > Number(end)) invalid("invalid byte range");
|
|
2167
|
+
return value;
|
|
2168
|
+
}
|
|
2169
|
+
function checkRange(message, range, metadata) {
|
|
2170
|
+
if (!range) {
|
|
2171
|
+
if (message.statusCode !== 200) malformed("unexpected GET status");
|
|
2172
|
+
return metadata.ContentLength;
|
|
2173
|
+
}
|
|
2174
|
+
if (message.statusCode !== 206) malformed("provider ignored byte range");
|
|
2175
|
+
const match = /^bytes ([0-9]+)-([0-9]+)\/([0-9]+)$/.exec(header(message, "content-range") ?? "");
|
|
2176
|
+
if (!match) malformed("invalid Content-Range");
|
|
2177
|
+
const start = integer2(match[1]);
|
|
2178
|
+
const end = integer2(match[2]);
|
|
2179
|
+
const total = integer2(match[3]);
|
|
2180
|
+
const [requestedStart, requestedEnd] = range.slice(6).split("-");
|
|
2181
|
+
const expectedStart = requestedStart ? Number(requestedStart) : Math.max(0, total - Number(requestedEnd));
|
|
2182
|
+
const expectedEnd = requestedStart && requestedEnd ? Math.min(total - 1, Number(requestedEnd)) : total - 1;
|
|
2183
|
+
if (start !== expectedStart || end !== expectedEnd || start > end || end >= total || metadata.ContentLength !== void 0 && metadata.ContentLength !== end - start + 1) malformed("response range differs from request");
|
|
2184
|
+
return end - start + 1;
|
|
2185
|
+
}
|
|
2186
|
+
function createS3HttpTransport(options) {
|
|
2187
|
+
if (!options || typeof options.endpoint !== "string" || !/^https?:\/\/[^\\/?#]+\/?$/.test(options.endpoint)) invalid("endpoint must be an explicit HTTP(S) origin without path, credentials, query or fragment");
|
|
2188
|
+
let endpoint;
|
|
2189
|
+
try {
|
|
2190
|
+
endpoint = new URL(options.endpoint);
|
|
2191
|
+
} catch {
|
|
2192
|
+
return invalid("invalid endpoint");
|
|
2193
|
+
}
|
|
2194
|
+
if (endpoint.username || endpoint.password) invalid("endpoint credentials are not allowed");
|
|
2195
|
+
if (endpoint.protocol === "http:" && !boolean(options.allowInsecureHttp, false, "allowInsecureHttp")) invalid("HTTP requires explicit allowInsecureHttp");
|
|
2196
|
+
if (typeof options.region !== "string" || !/^[a-z0-9-]{1,64}$/.test(options.region)) invalid("explicit signing region required");
|
|
2197
|
+
if (!options.credentials || typeof options.credentials !== "function" && typeof options.credentials !== "object") invalid("explicit credentials required");
|
|
2198
|
+
const credentials = typeof options.credentials === "function" ? options.credentials : Object.freeze({ ...options.credentials });
|
|
2199
|
+
const addressing = options.addressingStyle ?? "path";
|
|
2200
|
+
if (addressing !== "path" && addressing !== "virtual-hosted") invalid("unsupported addressing style");
|
|
2201
|
+
const listEncoding = options.listUrlEncoding ?? "percent";
|
|
2202
|
+
if (listEncoding !== "percent" && listEncoding !== "form") invalid("unsupported LIST URL encoding");
|
|
2203
|
+
if (addressing === "virtual-hosted" && (isIP(endpoint.hostname.replace(/^\[|\]$/g, "")) || endpoint.hostname === "localhost")) invalid("virtual-hosted addressing requires a DNS endpoint");
|
|
2204
|
+
const maxPut = limit(options.maxPutBytes, 64 * 1024 * 1024, "maxPutBytes");
|
|
2205
|
+
const maxGet = limit(options.maxGetBytes, 64 * 1024 * 1024, "maxGetBytes");
|
|
2206
|
+
const maxXml = limit(options.maxXmlBytes, 4 * 1024 * 1024, "maxXmlBytes", 16 * 1024 * 1024);
|
|
2207
|
+
const timeout = limit(options.requestTimeoutMs, 3e4, "requestTimeoutMs", 2147483647);
|
|
2208
|
+
const enabledCopy = boolean(options.enableCopy, true, "enableCopy");
|
|
2209
|
+
const verified = options.verifiedConditionalOperations;
|
|
2210
|
+
const conditionalPut = boolean(verified?.put, false, "verifiedConditionalOperations.put");
|
|
2211
|
+
const nativeConditionalCopy = boolean(verified?.copy, false, "verifiedConditionalOperations.copy");
|
|
2212
|
+
const conditionalCopy = enabledCopy ? nativeConditionalCopy : conditionalPut;
|
|
2213
|
+
const conditionalDelete = boolean(verified?.delete, false, "verifiedConditionalOperations.delete");
|
|
2214
|
+
const clock = options.clock ?? (() => /* @__PURE__ */ new Date());
|
|
2215
|
+
const factory = options.request;
|
|
2216
|
+
const region = options.region;
|
|
2217
|
+
if (typeof clock !== "function" || factory !== void 0 && typeof factory !== "function") invalid("invalid clock or request factory");
|
|
2218
|
+
const exchange = async (method, bucket, key, headers, query, body, requestOptions) => {
|
|
2219
|
+
requestOptions?.abortSignal?.throwIfAborted();
|
|
2220
|
+
bucketName(bucket);
|
|
2221
|
+
if (key !== void 0) objectKey(key);
|
|
2222
|
+
const hostname = addressing === "virtual-hosted" ? `${bucket}.${endpoint.hostname}` : endpoint.hostname;
|
|
2223
|
+
const host = `${hostname}${endpoint.port ? `:${endpoint.port}` : ""}`;
|
|
2224
|
+
const path = addressing === "virtual-hosted" ? `/${key === void 0 ? "" : uriEncode(key, true)}` : `/${bucket}${key === void 0 ? "" : `/${uriEncode(key, true)}`}`;
|
|
2225
|
+
const encodedQuery = canonicalQuery(query);
|
|
2226
|
+
const target = `${path}${encodedQuery ? `?${encodedQuery}` : ""}`;
|
|
2227
|
+
if (target.length > 16 * 1024) invalid("request target exceeds 16 KiB");
|
|
2228
|
+
const scope = scopeFor(requestOptions?.abortSignal, timeout);
|
|
2229
|
+
try {
|
|
2230
|
+
scope.signal.throwIfAborted();
|
|
2231
|
+
const resolved = typeof credentials === "function" ? await abortable(Promise.resolve(credentials({ signal: scope.signal })), scope.signal) : credentials;
|
|
2232
|
+
scope.signal.throwIfAborted();
|
|
2233
|
+
const signed = signRequest({
|
|
2234
|
+
method,
|
|
2235
|
+
path,
|
|
2236
|
+
query: encodedQuery,
|
|
2237
|
+
headers: { ...headers, host, ...method === "PUT" ? { "content-length": String(body.length) } : {} },
|
|
2238
|
+
body,
|
|
2239
|
+
region,
|
|
2240
|
+
credentials: resolved,
|
|
2241
|
+
date: clock()
|
|
2242
|
+
});
|
|
2243
|
+
const response = await sendRequest({
|
|
2244
|
+
protocol: endpoint.protocol,
|
|
2245
|
+
hostname: hostname.replace(/^\[|\]$/g, ""),
|
|
2246
|
+
...endpoint.port ? { port: endpoint.port } : {},
|
|
2247
|
+
method,
|
|
2248
|
+
path: target,
|
|
2249
|
+
headers: signed.headers,
|
|
2250
|
+
agent: false,
|
|
2251
|
+
maxHeaderSize: 16 * 1024,
|
|
2252
|
+
...{ rejectUnauthorized: true }
|
|
2253
|
+
}, body, scope, factory);
|
|
2254
|
+
const status = response.message.statusCode ?? 502;
|
|
2255
|
+
if (status < 200 || status >= 300) {
|
|
2256
|
+
const bytes = await collect(response, maxXml);
|
|
2257
|
+
const fallback = status === 404 ? "NotFound" : status === 403 || status === 401 ? "AccessDenied" : status === 412 ? "PreconditionFailed" : status === 409 ? "ConditionalRequestConflict" : status === 501 ? "NotImplemented" : status === 408 ? "RequestTimeout" : status >= 300 && status < 400 ? "RedirectRejected" : "HttpError";
|
|
2258
|
+
let code = fallback;
|
|
2259
|
+
let message = `S3 HTTP request failed with status ${status}`;
|
|
2260
|
+
if (bytes.length) {
|
|
2261
|
+
try {
|
|
2262
|
+
const parsed = parseXml(bytes);
|
|
2263
|
+
if (parsed.name === "Error") {
|
|
2264
|
+
code = text(parsed, "Code") || fallback;
|
|
2265
|
+
message = text(parsed, "Message") || message;
|
|
2266
|
+
}
|
|
2267
|
+
} catch (ignoredError) {
|
|
2268
|
+
void ignoredError;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
throw new S3ServiceError(code, status, message);
|
|
2272
|
+
}
|
|
2273
|
+
return response;
|
|
2274
|
+
} catch (error) {
|
|
2275
|
+
scope.finish();
|
|
2276
|
+
throw scope.signal.aborted ? scope.signal.reason : error;
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
const get = async (input, requestOptions) => {
|
|
2280
|
+
requestOptions?.abortSignal?.throwIfAborted();
|
|
2281
|
+
const range = rangeHeader(input.Range);
|
|
2282
|
+
const headers = { ...conditions(input), ...range ? { range } : {} };
|
|
2283
|
+
const response = await exchange("GET", input.Bucket, input.Key, headers, [], empty, requestOptions);
|
|
2284
|
+
try {
|
|
2285
|
+
const metadata = head(response.message);
|
|
2286
|
+
const expected = checkRange(response.message, range, metadata);
|
|
2287
|
+
if (input.IfMatch !== void 0 && input.IfMatch !== "*" && metadata.ETag !== void 0 && metadata.ETag !== input.IfMatch) malformed("GET ETag differs from IfMatch");
|
|
2288
|
+
return { ...metadata, Body: limitedBody(response, maxGet, expected) };
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
response.close();
|
|
2291
|
+
throw error;
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
const boundedCopy = async (input, requestOptions) => {
|
|
2295
|
+
if (!conditionalPut) throw new S3ServiceError("NotImplemented", 501, "disabled native COPY requires verified conditional PUT for bounded fallback");
|
|
2296
|
+
const scope = scopeFor(requestOptions?.abortSignal, timeout);
|
|
2297
|
+
const operationOptions = { abortSignal: scope.signal };
|
|
2298
|
+
const destination = { Bucket: input.Bucket, Key: input.Key };
|
|
2299
|
+
const source = copySource(input.CopySource).replace(/^\//, "");
|
|
2300
|
+
const slash = source.indexOf("/");
|
|
2301
|
+
const origin = { Bucket: decodeURIComponent(source.slice(0, slash)), Key: decodeURIComponent(source.slice(slash + 1)) };
|
|
2302
|
+
const sourceCondition = input.CopySourceIfMatch;
|
|
2303
|
+
let destinationCondition = { ...conditions(input) };
|
|
2304
|
+
const directive = input.MetadataDirective ?? "COPY";
|
|
2305
|
+
const replacementMetadata = input.Metadata === void 0 ? {} : { ...input.Metadata };
|
|
2306
|
+
let body;
|
|
2307
|
+
try {
|
|
2308
|
+
scope.signal.throwIfAborted();
|
|
2309
|
+
if (Object.keys(destinationCondition).length === 0) {
|
|
2310
|
+
let current;
|
|
2311
|
+
try {
|
|
2312
|
+
current = await transport.headObject(destination, operationOptions);
|
|
2313
|
+
} catch (error) {
|
|
2314
|
+
if (scope.signal.aborted) throw scope.signal.reason;
|
|
2315
|
+
if (!(error instanceof S3ServiceError) || !["NotFound", "NoSuchKey"].includes(error.code)) throw error;
|
|
2316
|
+
}
|
|
2317
|
+
if (current) {
|
|
2318
|
+
if (!current.ETag) malformed("destination lacks ETag for conditional COPY fallback");
|
|
2319
|
+
destinationCondition = { "if-match": current.ETag };
|
|
2320
|
+
} else destinationCondition = { "if-none-match": "*" };
|
|
2321
|
+
}
|
|
2322
|
+
const output = await get({ ...origin, ...sourceCondition === void 0 ? {} : { IfMatch: sourceCondition } }, operationOptions);
|
|
2323
|
+
body = output.Body[Symbol.asyncIterator]();
|
|
2324
|
+
if (!output.ETag || output.ContentLength === void 0) malformed("source lacks ETag/length for bounded COPY fallback");
|
|
2325
|
+
if (sourceCondition !== void 0 && sourceCondition !== "*" && output.ETag !== sourceCondition) throw new S3ServiceError("PreconditionFailed", 412, "source changed before COPY fallback");
|
|
2326
|
+
if (output.ContentLength > maxPut) throw new S3ServiceError("EntityTooLarge", 413, "COPY fallback exceeds PUT byte limit");
|
|
2327
|
+
const chunks = [];
|
|
2328
|
+
let count = 0;
|
|
2329
|
+
while (true) {
|
|
2330
|
+
const result = await body.next();
|
|
2331
|
+
if (result.done) break;
|
|
2332
|
+
count += result.value.length;
|
|
2333
|
+
if (count > maxPut) throw new S3ServiceError("EntityTooLarge", 413, "COPY fallback exceeds PUT byte limit");
|
|
2334
|
+
chunks.push(result.value);
|
|
2335
|
+
}
|
|
2336
|
+
scope.signal.throwIfAborted();
|
|
2337
|
+
const metadata = directive === "REPLACE" ? replacementMetadata : output.Metadata;
|
|
2338
|
+
const headers = { ...metadataHeaders(metadata), ...destinationCondition };
|
|
2339
|
+
const response = await exchange("PUT", destination.Bucket, destination.Key, headers, [], Buffer.concat(chunks, count), operationOptions);
|
|
2340
|
+
try {
|
|
2341
|
+
if (response.message.statusCode !== 200) malformed("unexpected COPY fallback PUT status");
|
|
2342
|
+
await collect(response, maxXml);
|
|
2343
|
+
const etag = header(response.message, "etag");
|
|
2344
|
+
if (!etag) malformed("COPY fallback PUT lacks confirmation ETag");
|
|
2345
|
+
return { CopyObjectResult: { ETag: etag } };
|
|
2346
|
+
} finally {
|
|
2347
|
+
response.close();
|
|
2348
|
+
}
|
|
2349
|
+
} finally {
|
|
2350
|
+
await body?.return?.();
|
|
2351
|
+
scope.finish();
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
const transport = {
|
|
2355
|
+
capabilities: Object.freeze({ streamingRead: true, streamingWrite: false, conditionalPut, conditionalCopy, conditionalDelete }),
|
|
2356
|
+
async headObject(input, requestOptions) {
|
|
2357
|
+
const response = await exchange("HEAD", input.Bucket, input.Key, {}, [], empty, requestOptions);
|
|
2358
|
+
try {
|
|
2359
|
+
if (response.message.statusCode !== 200) malformed("unexpected HEAD status");
|
|
2360
|
+
const metadata = head(response.message);
|
|
2361
|
+
await collect(response, maxXml);
|
|
2362
|
+
return metadata;
|
|
2363
|
+
} finally {
|
|
2364
|
+
response.close();
|
|
2365
|
+
}
|
|
2366
|
+
},
|
|
2367
|
+
async getObject(input, requestOptions) {
|
|
2368
|
+
const response = await get(input, requestOptions);
|
|
2369
|
+
const chunks = [];
|
|
2370
|
+
let count = 0;
|
|
2371
|
+
for await (const chunk of response.Body) {
|
|
2372
|
+
chunks.push(chunk);
|
|
2373
|
+
count += chunk.length;
|
|
2374
|
+
}
|
|
2375
|
+
return { ...response, Body: Buffer.concat(chunks, count) };
|
|
2376
|
+
},
|
|
2377
|
+
getObjectStream: get,
|
|
2378
|
+
async putObject(input, requestOptions) {
|
|
2379
|
+
requestOptions?.abortSignal?.throwIfAborted();
|
|
2380
|
+
if (!(input.Body instanceof Uint8Array)) invalid("PUT requires a bounded Uint8Array");
|
|
2381
|
+
if (input.Body.length > maxPut) throw new S3ServiceError("EntityTooLarge", 413, "PUT exceeds byte limit");
|
|
2382
|
+
requireCondition(conditionalPut, input.IfMatch !== void 0 || input.IfNoneMatch !== void 0);
|
|
2383
|
+
const body = new Uint8Array(input.Body);
|
|
2384
|
+
const headers = { ...metadataHeaders(input.Metadata), ...conditions(input) };
|
|
2385
|
+
const response = await exchange("PUT", input.Bucket, input.Key, headers, [], body, requestOptions);
|
|
2386
|
+
try {
|
|
2387
|
+
if (response.message.statusCode !== 200) malformed("unexpected PUT status");
|
|
2388
|
+
await collect(response, maxXml);
|
|
2389
|
+
return { ETag: header(response.message, "etag") };
|
|
2390
|
+
} finally {
|
|
2391
|
+
response.close();
|
|
2392
|
+
}
|
|
2393
|
+
},
|
|
2394
|
+
async deleteObject(input, requestOptions) {
|
|
2395
|
+
requestOptions?.abortSignal?.throwIfAborted();
|
|
2396
|
+
requireCondition(conditionalDelete, input.IfMatch !== void 0);
|
|
2397
|
+
const response = await exchange("DELETE", input.Bucket, input.Key, conditions(input), [], empty, requestOptions);
|
|
2398
|
+
try {
|
|
2399
|
+
if (response.message.statusCode !== 204) malformed("unexpected DELETE status");
|
|
2400
|
+
await collect(response, maxXml);
|
|
2401
|
+
return {};
|
|
2402
|
+
} finally {
|
|
2403
|
+
response.close();
|
|
2404
|
+
}
|
|
2405
|
+
},
|
|
2406
|
+
async copyObject(input, requestOptions) {
|
|
2407
|
+
requestOptions?.abortSignal?.throwIfAborted();
|
|
2408
|
+
if (input.MetadataDirective !== void 0 && input.MetadataDirective !== "COPY" && input.MetadataDirective !== "REPLACE") invalid("invalid MetadataDirective");
|
|
2409
|
+
const headers = {
|
|
2410
|
+
...metadataHeaders(input.Metadata),
|
|
2411
|
+
...conditions(input),
|
|
2412
|
+
"x-amz-copy-source": copySource(input.CopySource),
|
|
2413
|
+
...input.CopySourceIfMatch === void 0 ? {} : { "x-amz-copy-source-if-match": headerValue(input.CopySourceIfMatch) },
|
|
2414
|
+
...input.MetadataDirective === void 0 ? {} : { "x-amz-metadata-directive": input.MetadataDirective }
|
|
2415
|
+
};
|
|
2416
|
+
if (input.CopySourceIfMatch !== void 0 && !headers["x-amz-copy-source-if-match"]) invalid("empty CopySourceIfMatch");
|
|
2417
|
+
bucketName(input.Bucket);
|
|
2418
|
+
objectKey(input.Key);
|
|
2419
|
+
if (!enabledCopy) return boundedCopy(input, requestOptions);
|
|
2420
|
+
requireCondition(conditionalCopy, input.IfMatch !== void 0 || input.IfNoneMatch !== void 0 || input.CopySourceIfMatch !== void 0);
|
|
2421
|
+
const response = await exchange("PUT", input.Bucket, input.Key, headers, [], empty, requestOptions);
|
|
2422
|
+
const root = parseXml(await collect(response, maxXml));
|
|
2423
|
+
if (root.name === "Error") throw new S3ServiceError(text(root, "Code", true), response.message.statusCode, text(root, "Message") ?? "embedded COPY failure");
|
|
2424
|
+
if (response.message.statusCode !== 200 || root.name !== "CopyObjectResult") malformed("invalid COPY response");
|
|
2425
|
+
return { CopyObjectResult: { ETag: text(root, "ETag", true), LastModified: timestamp(text(root, "LastModified", true)) } };
|
|
2426
|
+
},
|
|
2427
|
+
async listObjectsV2(input, requestOptions) {
|
|
2428
|
+
requestOptions?.abortSignal?.throwIfAborted();
|
|
2429
|
+
if (input.MaxKeys !== void 0 && (!Number.isSafeInteger(input.MaxKeys) || input.MaxKeys < 0 || input.MaxKeys > 1e3)) invalid("MaxKeys must be 0..1000");
|
|
2430
|
+
const query = [["list-type", "2"], ["encoding-type", "url"]];
|
|
2431
|
+
for (const [name, value] of [["prefix", input.Prefix], ["delimiter", input.Delimiter], ["continuation-token", input.ContinuationToken]]) {
|
|
2432
|
+
if (value !== void 0) {
|
|
2433
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > 8192) invalid(`invalid ${name}`);
|
|
2434
|
+
query.push([name, value]);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (input.MaxKeys !== void 0) query.push(["max-keys", String(input.MaxKeys)]);
|
|
2438
|
+
const response = await exchange("GET", input.Bucket, void 0, {}, query, empty, requestOptions);
|
|
2439
|
+
const root = parseXml(await collect(response, maxXml));
|
|
2440
|
+
if (response.message.statusCode !== 200 || root.name !== "ListBucketResult") malformed("invalid LIST response");
|
|
2441
|
+
const encoding = text(root, "EncodingType");
|
|
2442
|
+
if (encoding !== void 0 && encoding !== "url") malformed("unsupported LIST encoding");
|
|
2443
|
+
const decode = (value) => {
|
|
2444
|
+
if (encoding !== "url") return value;
|
|
2445
|
+
try {
|
|
2446
|
+
return decodeURIComponent(listEncoding === "form" ? value.replace(/\+/g, " ") : value);
|
|
2447
|
+
} catch {
|
|
2448
|
+
return malformed("invalid URL-encoded LIST key");
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
const truncated = text(root, "IsTruncated", true);
|
|
2452
|
+
if (truncated !== "true" && truncated !== "false") malformed("invalid IsTruncated");
|
|
2453
|
+
const token = text(root, "NextContinuationToken");
|
|
2454
|
+
if (truncated === "true" && !token) malformed("truncated LIST lacks continuation token");
|
|
2455
|
+
const keyCount = text(root, "KeyCount");
|
|
2456
|
+
const contents = children(root, "Contents").map((item) => ({
|
|
2457
|
+
Key: decode(text(item, "Key", true)),
|
|
2458
|
+
Size: integer2(text(item, "Size", true)),
|
|
2459
|
+
ETag: text(item, "ETag", true),
|
|
2460
|
+
LastModified: timestamp(text(item, "LastModified", true))
|
|
2461
|
+
}));
|
|
2462
|
+
const prefixes = children(root, "CommonPrefixes").map((item) => ({ Prefix: decode(text(item, "Prefix", true)) }));
|
|
2463
|
+
if (contents.length + prefixes.length > (input.MaxKeys ?? 1e3)) malformed("LIST exceeds requested page size");
|
|
2464
|
+
return {
|
|
2465
|
+
Contents: contents,
|
|
2466
|
+
CommonPrefixes: prefixes,
|
|
2467
|
+
IsTruncated: truncated === "true",
|
|
2468
|
+
...keyCount === void 0 ? {} : { KeyCount: integer2(keyCount) },
|
|
2469
|
+
...token === void 0 ? {} : { NextContinuationToken: token }
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
};
|
|
2473
|
+
return Object.freeze(transport);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// packages/safe-fs/src/node/filesystem.ts
|
|
2477
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
2478
|
+
import { randomBytes } from "node:crypto";
|
|
2479
|
+
import { posix as posix3 } from "node:path";
|
|
2480
|
+
import { fileURLToPath } from "node:url";
|
|
2481
|
+
function createNodeFsBridge(fs, options = {}) {
|
|
2482
|
+
return new FileSystemBridge(fs, options, {
|
|
2483
|
+
codec: {
|
|
2484
|
+
isEncoding: Buffer2.isEncoding,
|
|
2485
|
+
encode(text2, encoding) {
|
|
2486
|
+
if (!Buffer2.isEncoding(encoding)) throw new TypeError("Invalid encoding");
|
|
2487
|
+
return Buffer2.from(text2, encoding);
|
|
2488
|
+
},
|
|
2489
|
+
decode(bytes, encoding) {
|
|
2490
|
+
if (!Buffer2.isEncoding(encoding)) throw new TypeError("Invalid encoding");
|
|
2491
|
+
return Buffer2.from(bytes).toString(encoding);
|
|
2492
|
+
}
|
|
2493
|
+
},
|
|
2494
|
+
copyBytes: Buffer2.from,
|
|
2495
|
+
pathValue(value) {
|
|
2496
|
+
return value instanceof URL ? fileURLToPath(value) : Buffer2.isBuffer(value) ? value.toString("utf8") : value;
|
|
2497
|
+
},
|
|
2498
|
+
randomSuffix() {
|
|
2499
|
+
return randomBytes(6).toString("hex").slice(0, 6);
|
|
2500
|
+
},
|
|
2501
|
+
paths: posix3
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
// packages/safe-fs/src/config.ts
|
|
2506
|
+
function readConfigRecord(value, label, keys) {
|
|
2507
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2508
|
+
throw new TypeError(`${label} must be a record.`);
|
|
2509
|
+
}
|
|
2510
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2511
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
2512
|
+
throw new TypeError(`${label} must be a plain record.`);
|
|
2513
|
+
}
|
|
2514
|
+
const record = /* @__PURE__ */ Object.create(null);
|
|
2515
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
2516
|
+
if (typeof key !== "string" || keys !== void 0 && !keys.includes(key)) {
|
|
2517
|
+
throw new TypeError(`Unknown ${label}: ${String(key)}`);
|
|
2518
|
+
}
|
|
2519
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
2520
|
+
if (!("value" in property)) throw new TypeError(`${label}.${key} must be a data property.`);
|
|
2521
|
+
record[key] = property.value;
|
|
2522
|
+
}
|
|
2523
|
+
return Object.freeze(record);
|
|
2524
|
+
}
|
|
2525
|
+
function validateFileSystemConfig(value) {
|
|
2526
|
+
const config = readConfigRecord(value, "filesystem config", ["type", "options"]);
|
|
2527
|
+
if (typeof config.type !== "string" || config.type.trim().length === 0) {
|
|
2528
|
+
throw new TypeError("Filesystem adapter type must be a nonempty string.");
|
|
2529
|
+
}
|
|
2530
|
+
return {
|
|
2531
|
+
type: config.type,
|
|
2532
|
+
...config.options === void 0 ? {} : { options: readConfigRecord(config.options, "filesystem options") }
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
async function createFileSystem(config, options) {
|
|
2536
|
+
const validated = validateFileSystemConfig(config);
|
|
2537
|
+
const creation = readConfigRecord(options, "filesystem creation option", ["registry"]);
|
|
2538
|
+
const registry = creation.registry;
|
|
2539
|
+
if (registry === void 0 || registry === null || typeof registry.get !== "function") {
|
|
2540
|
+
throw new TypeError("registry must be a filesystem adapter map.");
|
|
2541
|
+
}
|
|
2542
|
+
const descriptor = registry.get(validated.type);
|
|
2543
|
+
if (descriptor === void 0)
|
|
2544
|
+
throw new TypeError(`Unknown filesystem adapter: ${validated.type}`);
|
|
2545
|
+
const adapterOptions = validated.options ?? Object.freeze(/* @__PURE__ */ Object.create(null));
|
|
2546
|
+
descriptor.validateOptions(adapterOptions);
|
|
2547
|
+
return await descriptor.create(adapterOptions);
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// packages/safe-fs/src/config/memory.ts
|
|
2551
|
+
function createMemoryFileSystemAdapter(create) {
|
|
2552
|
+
return {
|
|
2553
|
+
validateOptions(options) {
|
|
2554
|
+
readConfigRecord(options, "memory option", []);
|
|
2555
|
+
},
|
|
2556
|
+
create
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// packages/safe-fs/src/config/real.ts
|
|
2561
|
+
function createRealFileSystemAdapter(create) {
|
|
2562
|
+
return {
|
|
2563
|
+
validateOptions(options) {
|
|
2564
|
+
const record = readConfigRecord(options, "real option", ["root"]);
|
|
2565
|
+
if (typeof record.root !== "string" || !record.root.startsWith("/") || record.root.includes("\0")) {
|
|
2566
|
+
throw new TypeError("adapter.options.root must be an absolute host directory");
|
|
2567
|
+
}
|
|
2568
|
+
},
|
|
2569
|
+
create(options) {
|
|
2570
|
+
return create({ root: options.root });
|
|
2571
|
+
}
|
|
2572
|
+
};
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
// packages/safe-fs/src/config.node.ts
|
|
2576
|
+
function createNodeFileSystemAdapterRegistry(extensions) {
|
|
2577
|
+
if (extensions !== void 0 && (extensions === null || typeof extensions.get !== "function" || typeof extensions[Symbol.iterator] !== "function")) {
|
|
2578
|
+
throw new TypeError("registry must be a filesystem adapter map.");
|
|
2579
|
+
}
|
|
2580
|
+
const registry = /* @__PURE__ */ new Map([
|
|
2581
|
+
["memory", createMemoryFileSystemAdapter(createMemoryFileSystem)],
|
|
2582
|
+
["real", createRealFileSystemAdapter(createRealFileSystem)]
|
|
2583
|
+
]);
|
|
2584
|
+
for (const [name, descriptor] of extensions ?? []) {
|
|
2585
|
+
if (registry.has(name)) throw new TypeError(`Filesystem adapter already registered: ${name}`);
|
|
2586
|
+
registry.set(name, descriptor);
|
|
2587
|
+
}
|
|
2588
|
+
return registry;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
export {
|
|
2592
|
+
posixPath,
|
|
2593
|
+
basename,
|
|
2594
|
+
dirname,
|
|
2595
|
+
extname,
|
|
2596
|
+
joinPath,
|
|
2597
|
+
isAbsolutePath,
|
|
2598
|
+
RealFileSystem,
|
|
2599
|
+
createRealFileSystem,
|
|
2600
|
+
createS3Transport,
|
|
2601
|
+
S3ServiceError,
|
|
2602
|
+
encodeCopySource,
|
|
2603
|
+
S3RenameError,
|
|
2604
|
+
S3FileSystem,
|
|
2605
|
+
MockS3Client,
|
|
2606
|
+
createS3HttpTransport,
|
|
2607
|
+
createNodeFsBridge,
|
|
2608
|
+
readConfigRecord,
|
|
2609
|
+
validateFileSystemConfig,
|
|
2610
|
+
createFileSystem,
|
|
2611
|
+
createNodeFileSystemAdapterRegistry
|
|
2612
|
+
};
|
|
2613
|
+
//# sourceMappingURL=chunk-7M2SJPSZ.js.map
|