@backblaze-labs/b2-sdk 0.2.0 → 0.3.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/CHANGELOG.md +42 -1
- package/README.md +20 -2
- package/dist/_virtual/_b2-sdk-version-json.cjs +1 -1
- package/dist/_virtual/_b2-sdk-version-json.js +1 -1
- package/dist/auth/file.cjs.map +1 -1
- package/dist/backup/client.cjs +206 -0
- package/dist/backup/client.cjs.map +1 -0
- package/dist/backup/client.d.cts +238 -0
- package/dist/backup/client.d.cts.map +1 -0
- package/dist/backup/client.d.ts +238 -0
- package/dist/backup/client.d.ts.map +1 -0
- package/dist/backup/client.js +206 -0
- package/dist/backup/client.js.map +1 -0
- package/dist/backup/index.cjs +15 -0
- package/dist/backup/index.d.cts +22 -0
- package/dist/backup/index.d.cts.map +1 -0
- package/dist/backup/index.d.ts +22 -0
- package/dist/backup/index.d.ts.map +1 -0
- package/dist/backup/index.js +7 -0
- package/dist/backup/raw.cjs +146 -0
- package/dist/backup/raw.cjs.map +1 -0
- package/dist/backup/raw.d.cts +122 -0
- package/dist/backup/raw.d.cts.map +1 -0
- package/dist/backup/raw.d.ts +122 -0
- package/dist/backup/raw.d.ts.map +1 -0
- package/dist/backup/raw.js +146 -0
- package/dist/backup/raw.js.map +1 -0
- package/dist/bucket.cjs +2 -1
- package/dist/bucket.cjs.map +1 -1
- package/dist/bucket.js +2 -1
- package/dist/bucket.js.map +1 -1
- package/dist/copy/large.cjs +5 -3
- package/dist/copy/large.cjs.map +1 -1
- package/dist/copy/large.d.cts.map +1 -1
- package/dist/copy/large.d.ts.map +1 -1
- package/dist/copy/large.js +5 -3
- package/dist/copy/large.js.map +1 -1
- package/dist/download/checksum.cjs +6 -2
- package/dist/download/checksum.cjs.map +1 -1
- package/dist/download/checksum.js +6 -2
- package/dist/download/checksum.js.map +1 -1
- package/dist/download/parallel.cjs +52 -15
- package/dist/download/parallel.cjs.map +1 -1
- package/dist/download/parallel.d.cts +8 -1
- package/dist/download/parallel.d.cts.map +1 -1
- package/dist/download/parallel.d.ts +8 -1
- package/dist/download/parallel.d.ts.map +1 -1
- package/dist/download/parallel.js +52 -15
- package/dist/download/parallel.js.map +1 -1
- package/dist/download/single.cjs +141 -6
- package/dist/download/single.cjs.map +1 -1
- package/dist/download/single.d.cts.map +1 -1
- package/dist/download/single.d.ts.map +1 -1
- package/dist/download/single.js +141 -6
- package/dist/download/single.js.map +1 -1
- package/dist/errors/index.cjs +173 -3
- package/dist/errors/index.cjs.map +1 -1
- package/dist/errors/index.d.cts +119 -0
- package/dist/errors/index.d.cts.map +1 -1
- package/dist/errors/index.d.ts +119 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +161 -4
- package/dist/errors/index.js.map +1 -1
- package/dist/http/retry.cjs +1 -1
- package/dist/http/retry.cjs.map +1 -1
- package/dist/http/retry.js +1 -1
- package/dist/http/retry.js.map +1 -1
- package/dist/http/transport.cjs +61 -23
- package/dist/http/transport.cjs.map +1 -1
- package/dist/http/transport.d.cts +25 -3
- package/dist/http/transport.d.cts.map +1 -1
- package/dist/http/transport.d.ts +25 -3
- package/dist/http/transport.d.ts.map +1 -1
- package/dist/http/transport.js +59 -22
- package/dist/http/transport.js.map +1 -1
- package/dist/index.cjs +25 -2
- package/dist/index.d.cts +6 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/object.cjs +1 -1
- package/dist/object.cjs.map +1 -1
- package/dist/object.d.cts +4 -2
- package/dist/object.d.cts.map +1 -1
- package/dist/object.d.ts +4 -2
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +1 -1
- package/dist/object.js.map +1 -1
- package/dist/partner/account-info.d.cts +43 -0
- package/dist/partner/account-info.d.cts.map +1 -0
- package/dist/partner/account-info.d.ts +43 -0
- package/dist/partner/account-info.d.ts.map +1 -0
- package/dist/partner/auth-clone.cjs +64 -0
- package/dist/partner/auth-clone.cjs.map +1 -0
- package/dist/partner/auth-clone.d.cts +27 -0
- package/dist/partner/auth-clone.d.cts.map +1 -0
- package/dist/partner/auth-clone.d.ts +27 -0
- package/dist/partner/auth-clone.d.ts.map +1 -0
- package/dist/partner/auth-clone.js +63 -0
- package/dist/partner/auth-clone.js.map +1 -0
- package/dist/partner/auth-core.cjs +175 -0
- package/dist/partner/auth-core.cjs.map +1 -0
- package/dist/partner/auth-core.d.cts +111 -0
- package/dist/partner/auth-core.d.cts.map +1 -0
- package/dist/partner/auth-core.d.ts +111 -0
- package/dist/partner/auth-core.d.ts.map +1 -0
- package/dist/partner/auth-core.js +175 -0
- package/dist/partner/auth-core.js.map +1 -0
- package/dist/partner/auth-shape.cjs +36 -0
- package/dist/partner/auth-shape.cjs.map +1 -0
- package/dist/partner/auth-shape.d.cts +12 -0
- package/dist/partner/auth-shape.d.cts.map +1 -0
- package/dist/partner/auth-shape.d.ts +12 -0
- package/dist/partner/auth-shape.d.ts.map +1 -0
- package/dist/partner/auth-shape.js +36 -0
- package/dist/partner/auth-shape.js.map +1 -0
- package/dist/partner/client.cjs +255 -0
- package/dist/partner/client.cjs.map +1 -0
- package/dist/partner/client.d.cts +335 -0
- package/dist/partner/client.d.cts.map +1 -0
- package/dist/partner/client.d.ts +335 -0
- package/dist/partner/client.d.ts.map +1 -0
- package/dist/partner/client.js +255 -0
- package/dist/partner/client.js.map +1 -0
- package/dist/partner/endpoint-url.cjs +108 -0
- package/dist/partner/endpoint-url.cjs.map +1 -0
- package/dist/partner/endpoint-url.d.cts +72 -0
- package/dist/partner/endpoint-url.d.cts.map +1 -0
- package/dist/partner/endpoint-url.d.ts +72 -0
- package/dist/partner/endpoint-url.d.ts.map +1 -0
- package/dist/partner/endpoint-url.js +105 -0
- package/dist/partner/endpoint-url.js.map +1 -0
- package/dist/partner/in-memory.cjs +135 -0
- package/dist/partner/in-memory.cjs.map +1 -0
- package/dist/partner/in-memory.d.cts +99 -0
- package/dist/partner/in-memory.d.cts.map +1 -0
- package/dist/partner/in-memory.d.ts +99 -0
- package/dist/partner/in-memory.d.ts.map +1 -0
- package/dist/partner/in-memory.js +135 -0
- package/dist/partner/in-memory.js.map +1 -0
- package/dist/partner/index.cjs +18 -0
- package/dist/partner/index.d.cts +23 -0
- package/dist/partner/index.d.cts.map +1 -0
- package/dist/partner/index.d.ts +23 -0
- package/dist/partner/index.d.ts.map +1 -0
- package/dist/partner/index.js +7 -0
- package/dist/partner/raw.cjs +392 -0
- package/dist/partner/raw.cjs.map +1 -0
- package/dist/partner/raw.d.cts +223 -0
- package/dist/partner/raw.d.cts.map +1 -0
- package/dist/partner/raw.d.ts +223 -0
- package/dist/partner/raw.d.ts.map +1 -0
- package/dist/partner/raw.js +390 -0
- package/dist/partner/raw.js.map +1 -0
- package/dist/partner/redaction-placeholders.cjs +10 -0
- package/dist/partner/redaction-placeholders.cjs.map +1 -0
- package/dist/partner/redaction-placeholders.d.cts +5 -0
- package/dist/partner/redaction-placeholders.d.cts.map +1 -0
- package/dist/partner/redaction-placeholders.d.ts +5 -0
- package/dist/partner/redaction-placeholders.d.ts.map +1 -0
- package/dist/partner/redaction-placeholders.js +9 -0
- package/dist/partner/redaction-placeholders.js.map +1 -0
- package/dist/partner/redaction.cjs +267 -0
- package/dist/partner/redaction.cjs.map +1 -0
- package/dist/partner/redaction.d.cts +124 -0
- package/dist/partner/redaction.d.cts.map +1 -0
- package/dist/partner/redaction.d.ts +124 -0
- package/dist/partner/redaction.d.ts.map +1 -0
- package/dist/partner/redaction.js +257 -0
- package/dist/partner/redaction.js.map +1 -0
- package/dist/raw/index.cjs +81 -19
- package/dist/raw/index.cjs.map +1 -1
- package/dist/raw/index.d.cts +6 -2
- package/dist/raw/index.d.cts.map +1 -1
- package/dist/raw/index.d.ts +6 -2
- package/dist/raw/index.d.ts.map +1 -1
- package/dist/raw/index.js +81 -19
- package/dist/raw/index.js.map +1 -1
- package/dist/raw/url.cjs +111 -0
- package/dist/raw/url.cjs.map +1 -0
- package/dist/raw/url.d.cts +41 -0
- package/dist/raw/url.d.cts.map +1 -0
- package/dist/raw/url.d.ts +41 -0
- package/dist/raw/url.d.ts.map +1 -0
- package/dist/raw/url.js +110 -0
- package/dist/raw/url.js.map +1 -0
- package/dist/s3/sigv4.cjs +15 -10
- package/dist/s3/sigv4.cjs.map +1 -1
- package/dist/s3/sigv4.js +15 -10
- package/dist/s3/sigv4.js.map +1 -1
- package/dist/simulator/index.cjs +1197 -206
- package/dist/simulator/index.cjs.map +1 -1
- package/dist/simulator/index.d.cts +109 -11
- package/dist/simulator/index.d.cts.map +1 -1
- package/dist/simulator/index.d.ts +109 -11
- package/dist/simulator/index.d.ts.map +1 -1
- package/dist/simulator/index.js +1196 -209
- package/dist/simulator/index.js.map +1 -1
- package/dist/simulator/partner.cjs +719 -0
- package/dist/simulator/partner.cjs.map +1 -0
- package/dist/simulator/partner.d.cts +183 -0
- package/dist/simulator/partner.d.cts.map +1 -0
- package/dist/simulator/partner.d.ts +183 -0
- package/dist/simulator/partner.d.ts.map +1 -0
- package/dist/simulator/partner.js +718 -0
- package/dist/simulator/partner.js.map +1 -0
- package/dist/simulator/validation.cjs +577 -0
- package/dist/simulator/validation.cjs.map +1 -1
- package/dist/simulator/validation.d.cts +117 -0
- package/dist/simulator/validation.d.cts.map +1 -1
- package/dist/simulator/validation.d.ts +117 -0
- package/dist/simulator/validation.d.ts.map +1 -1
- package/dist/simulator/validation.js +563 -1
- package/dist/simulator/validation.js.map +1 -1
- package/dist/streams/file-source.cjs +1 -1
- package/dist/streams/file-source.cjs.map +1 -1
- package/dist/streams/file-source.js +1 -1
- package/dist/streams/file-source.js.map +1 -1
- package/dist/streams/index.cjs +1 -1
- package/dist/streams/index.js +1 -1
- package/dist/streams/source.cjs +2 -1
- package/dist/streams/source.cjs.map +1 -1
- package/dist/streams/source.js +2 -1
- package/dist/streams/source.js.map +1 -1
- package/dist/sync/b2-sha1-reader.cjs +6 -2
- package/dist/sync/b2-sha1-reader.cjs.map +1 -1
- package/dist/sync/b2-sha1-reader.d.cts.map +1 -1
- package/dist/sync/b2-sha1-reader.d.ts.map +1 -1
- package/dist/sync/b2-sha1-reader.js +6 -2
- package/dist/sync/b2-sha1-reader.js.map +1 -1
- package/dist/sync/download-staging.cjs +11 -10
- package/dist/sync/download-staging.cjs.map +1 -1
- package/dist/sync/download-staging.d.cts +2 -1
- package/dist/sync/download-staging.d.cts.map +1 -1
- package/dist/sync/download-staging.d.ts +2 -1
- package/dist/sync/download-staging.d.ts.map +1 -1
- package/dist/sync/download-staging.js +11 -10
- package/dist/sync/download-staging.js.map +1 -1
- package/dist/sync/filters.cjs.map +1 -1
- package/dist/sync/filters.js.map +1 -1
- package/dist/sync/local-file-io.cjs +11 -6
- package/dist/sync/local-file-io.cjs.map +1 -1
- package/dist/sync/local-file-io.d.cts +1 -0
- package/dist/sync/local-file-io.d.cts.map +1 -1
- package/dist/sync/local-file-io.d.ts +1 -0
- package/dist/sync/local-file-io.d.ts.map +1 -1
- package/dist/sync/local-file-io.js +11 -6
- package/dist/sync/local-file-io.js.map +1 -1
- package/dist/sync/local-sha1.cjs +6 -15
- package/dist/sync/local-sha1.cjs.map +1 -1
- package/dist/sync/local-sha1.d.cts +0 -8
- package/dist/sync/local-sha1.d.cts.map +1 -1
- package/dist/sync/local-sha1.d.ts +0 -8
- package/dist/sync/local-sha1.d.ts.map +1 -1
- package/dist/sync/local-sha1.js +7 -15
- package/dist/sync/local-sha1.js.map +1 -1
- package/dist/sync/policies/compare.cjs +5 -3
- package/dist/sync/policies/compare.cjs.map +1 -1
- package/dist/sync/policies/compare.d.cts.map +1 -1
- package/dist/sync/policies/compare.d.ts.map +1 -1
- package/dist/sync/policies/compare.js +6 -4
- package/dist/sync/policies/compare.js.map +1 -1
- package/dist/sync/policies/index.cjs +4 -10
- package/dist/sync/policies/index.cjs.map +1 -1
- package/dist/sync/policies/index.js +4 -10
- package/dist/sync/policies/index.js.map +1 -1
- package/dist/sync/scanners/b2.cjs +2 -2
- package/dist/sync/scanners/b2.cjs.map +1 -1
- package/dist/sync/scanners/b2.js +2 -2
- package/dist/sync/scanners/b2.js.map +1 -1
- package/dist/sync/scanners/local.cjs.map +1 -1
- package/dist/sync/scanners/local.js.map +1 -1
- package/dist/sync/synchronizer.cjs +3 -2
- package/dist/sync/synchronizer.cjs.map +1 -1
- package/dist/sync/synchronizer.js +3 -2
- package/dist/sync/synchronizer.js.map +1 -1
- package/dist/types/auth.cjs +4 -0
- package/dist/types/auth.cjs.map +1 -1
- package/dist/types/auth.d.cts +4 -0
- package/dist/types/auth.d.cts.map +1 -1
- package/dist/types/auth.d.ts +4 -0
- package/dist/types/auth.d.ts.map +1 -1
- package/dist/types/auth.js +4 -0
- package/dist/types/auth.js.map +1 -1
- package/dist/types/backup.d.cts +80 -0
- package/dist/types/backup.d.cts.map +1 -0
- package/dist/types/backup.d.ts +80 -0
- package/dist/types/backup.d.ts.map +1 -0
- package/dist/types/bucket.cjs.map +1 -1
- package/dist/types/bucket.d.cts +4 -2
- package/dist/types/bucket.d.cts.map +1 -1
- package/dist/types/bucket.d.ts +4 -2
- package/dist/types/bucket.d.ts.map +1 -1
- package/dist/types/bucket.js.map +1 -1
- package/dist/types/download.cjs +62 -0
- package/dist/types/download.cjs.map +1 -0
- package/dist/types/download.d.cts +98 -0
- package/dist/types/download.d.cts.map +1 -1
- package/dist/types/download.d.ts +98 -0
- package/dist/types/download.d.ts.map +1 -1
- package/dist/types/download.js +61 -0
- package/dist/types/download.js.map +1 -0
- package/dist/types/encryption.cjs +58 -229
- package/dist/types/encryption.cjs.map +1 -1
- package/dist/types/encryption.d.cts +32 -0
- package/dist/types/encryption.d.cts.map +1 -1
- package/dist/types/encryption.d.ts +32 -0
- package/dist/types/encryption.d.ts.map +1 -1
- package/dist/types/encryption.js +56 -229
- package/dist/types/encryption.js.map +1 -1
- package/dist/types/errors.cjs +9 -0
- package/dist/types/errors.cjs.map +1 -1
- package/dist/types/errors.d.cts +1 -1
- package/dist/types/errors.d.cts.map +1 -1
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +9 -0
- package/dist/types/errors.js.map +1 -1
- package/dist/types/file.cjs.map +1 -1
- package/dist/types/file.d.cts +15 -2
- package/dist/types/file.d.cts.map +1 -1
- package/dist/types/file.d.ts +15 -2
- package/dist/types/file.d.ts.map +1 -1
- package/dist/types/file.js.map +1 -1
- package/dist/types/ids.cjs +30 -0
- package/dist/types/ids.cjs.map +1 -1
- package/dist/types/ids.d.cts +32 -0
- package/dist/types/ids.d.cts.map +1 -1
- package/dist/types/ids.d.ts +32 -0
- package/dist/types/ids.d.ts.map +1 -1
- package/dist/types/ids.js +28 -1
- package/dist/types/ids.js.map +1 -1
- package/dist/types/index.d.cts +10 -6
- package/dist/types/index.d.cts.map +1 -1
- package/dist/types/index.d.ts +10 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lock.cjs.map +1 -1
- package/dist/types/lock.d.cts +14 -0
- package/dist/types/lock.d.cts.map +1 -1
- package/dist/types/lock.d.ts +14 -0
- package/dist/types/lock.d.ts.map +1 -1
- package/dist/types/lock.js.map +1 -1
- package/dist/types/notifications.cjs +1 -1
- package/dist/types/notifications.cjs.map +1 -1
- package/dist/types/notifications.d.cts +5 -3
- package/dist/types/notifications.d.cts.map +1 -1
- package/dist/types/notifications.d.ts +5 -3
- package/dist/types/notifications.d.ts.map +1 -1
- package/dist/types/notifications.js +1 -1
- package/dist/types/notifications.js.map +1 -1
- package/dist/types/partner.cjs +32 -0
- package/dist/types/partner.cjs.map +1 -0
- package/dist/types/partner.d.cts +394 -0
- package/dist/types/partner.d.cts.map +1 -0
- package/dist/types/partner.d.ts +394 -0
- package/dist/types/partner.d.ts.map +1 -0
- package/dist/types/partner.js +29 -0
- package/dist/types/partner.js.map +1 -0
- package/dist/types/upload.d.cts +31 -18
- package/dist/types/upload.d.cts.map +1 -1
- package/dist/types/upload.d.ts +31 -18
- package/dist/types/upload.d.ts.map +1 -1
- package/dist/upload/abort-scope.cjs +0 -45
- package/dist/upload/abort-scope.cjs.map +1 -1
- package/dist/upload/abort-scope.d.cts +0 -22
- package/dist/upload/abort-scope.d.cts.map +1 -1
- package/dist/upload/abort-scope.d.ts +0 -22
- package/dist/upload/abort-scope.d.ts.map +1 -1
- package/dist/upload/abort-scope.js +1 -44
- package/dist/upload/abort-scope.js.map +1 -1
- package/dist/upload/finish.cjs +3 -8
- package/dist/upload/finish.cjs.map +1 -1
- package/dist/upload/finish.d.cts.map +1 -1
- package/dist/upload/finish.d.ts.map +1 -1
- package/dist/upload/finish.js +1 -6
- package/dist/upload/finish.js.map +1 -1
- package/dist/upload/large.cjs +5 -1
- package/dist/upload/large.cjs.map +1 -1
- package/dist/upload/large.d.cts +16 -2
- package/dist/upload/large.d.cts.map +1 -1
- package/dist/upload/large.d.ts +16 -2
- package/dist/upload/large.d.ts.map +1 -1
- package/dist/upload/large.js +5 -1
- package/dist/upload/large.js.map +1 -1
- package/dist/upload/options.cjs.map +1 -1
- package/dist/upload/options.d.cts +12 -0
- package/dist/upload/options.d.cts.map +1 -1
- package/dist/upload/options.d.ts +12 -0
- package/dist/upload/options.d.ts.map +1 -1
- package/dist/upload/options.js.map +1 -1
- package/dist/upload/resume.cjs +2 -1
- package/dist/upload/resume.cjs.map +1 -1
- package/dist/upload/resume.d.cts +3 -1
- package/dist/upload/resume.d.cts.map +1 -1
- package/dist/upload/resume.d.ts +3 -1
- package/dist/upload/resume.d.ts.map +1 -1
- package/dist/upload/resume.js +2 -1
- package/dist/upload/resume.js.map +1 -1
- package/dist/upload/retry.cjs +8 -3
- package/dist/upload/retry.cjs.map +1 -1
- package/dist/upload/retry.js +8 -3
- package/dist/upload/retry.js.map +1 -1
- package/dist/upload/single.cjs +2 -1
- package/dist/upload/single.cjs.map +1 -1
- package/dist/upload/single.d.cts +6 -0
- package/dist/upload/single.d.cts.map +1 -1
- package/dist/upload/single.d.ts +6 -0
- package/dist/upload/single.d.ts.map +1 -1
- package/dist/upload/single.js +2 -1
- package/dist/upload/single.js.map +1 -1
- package/dist/upload/stream.cjs +6 -4
- package/dist/upload/stream.cjs.map +1 -1
- package/dist/upload/stream.d.cts +6 -0
- package/dist/upload/stream.d.cts.map +1 -1
- package/dist/upload/stream.d.ts +6 -0
- package/dist/upload/stream.d.ts.map +1 -1
- package/dist/upload/stream.js +3 -1
- package/dist/upload/stream.js.map +1 -1
- package/dist/util/abort.cjs +114 -0
- package/dist/util/abort.cjs.map +1 -0
- package/dist/util/abort.d.cts +65 -0
- package/dist/util/abort.d.cts.map +1 -0
- package/dist/util/abort.d.ts +65 -0
- package/dist/util/abort.d.ts.map +1 -0
- package/dist/util/abort.js +109 -0
- package/dist/util/abort.js.map +1 -0
- package/dist/util/defaults.cjs +3 -0
- package/dist/util/defaults.cjs.map +1 -1
- package/dist/util/defaults.d.cts +2 -0
- package/dist/util/defaults.d.cts.map +1 -1
- package/dist/util/defaults.d.ts +2 -0
- package/dist/util/defaults.d.ts.map +1 -1
- package/dist/util/defaults.js +3 -1
- package/dist/util/defaults.js.map +1 -1
- package/dist/util/md5.cjs +211 -0
- package/dist/util/md5.cjs.map +1 -0
- package/dist/util/md5.d.cts +43 -0
- package/dist/util/md5.d.cts.map +1 -0
- package/dist/util/md5.d.ts +43 -0
- package/dist/util/md5.d.ts.map +1 -0
- package/dist/util/md5.js +209 -0
- package/dist/util/md5.js.map +1 -0
- package/dist/util/normalize.cjs +5 -5
- package/dist/util/normalize.cjs.map +1 -1
- package/dist/util/normalize.d.cts +7 -7
- package/dist/util/normalize.d.cts.map +1 -1
- package/dist/util/normalize.d.ts +7 -7
- package/dist/util/normalize.d.ts.map +1 -1
- package/dist/util/normalize.js +5 -5
- package/dist/util/normalize.js.map +1 -1
- package/dist/util/paginator.cjs +7 -1
- package/dist/util/paginator.cjs.map +1 -1
- package/dist/util/paginator.d.cts +3 -1
- package/dist/util/paginator.d.cts.map +1 -1
- package/dist/util/paginator.d.ts +3 -1
- package/dist/util/paginator.d.ts.map +1 -1
- package/dist/util/paginator.js +7 -1
- package/dist/util/paginator.js.map +1 -1
- package/package.json +38 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","names":[],"sources":["../../src/simulator/validation.ts"],"sourcesContent":["/**\n * B2 spec-compliance validation helpers for the simulator.\n *\n * Each function mirrors a documented B2 input limit (file name length,\n * bucket name regex, file-info byte budget, etc.). Validators return\n * `null` for valid input or a `{ code, message }` pair that the handler\n * converts into a synthetic `400`/`403` response with the right B2\n * error code.\n *\n * Keeping validation in one module makes the simulator's \"what does B2\n * reject?\" surface auditable in a single file. Doc references inline at\n * each rule so the spec source is immediate.\n *\n * @packageDocumentation\n */\n\nimport {\n BUCKET_NAME_MAX,\n BUCKET_NAME_MIN,\n BUCKET_NAME_RESERVED_PREFIX,\n FILE_NAME_MAX_BYTES,\n getB2FileNameByteLength,\n hasB2FileNameControlCharacter,\n hasValidB2BucketNameShape,\n isB2BucketNameIpv4Address,\n} from '../internal/b2-naming.ts'\nimport { utf8Encoder } from '../util/text-codec.ts'\n\n/** Shape returned by validation functions when input is rejected. */\nexport interface ValidationError {\n readonly code: string\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Bucket name (`b2_create_bucket`, `b2_update_bucket`)\n// ---------------------------------------------------------------------------\n\nexport { BUCKET_NAME_MAX, BUCKET_NAME_MIN }\n\n/**\n * Validates a bucket name against B2's documented rules.\n *\n * @param name - Caller-supplied bucket name.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-create-bucket\n */\nexport function validateBucketName(name: string): ValidationError | null {\n if (typeof name !== 'string' || name.length < BUCKET_NAME_MIN || name.length > BUCKET_NAME_MAX) {\n return {\n code: 'invalid_bucket_name',\n message: `bucketName must be ${BUCKET_NAME_MIN}-${BUCKET_NAME_MAX} characters`,\n }\n }\n if (isB2BucketNameIpv4Address(name)) {\n return {\n code: 'invalid_bucket_name',\n message: 'bucketName must not be formatted as an IPv4 address',\n }\n }\n if (!hasValidB2BucketNameShape(name)) {\n return {\n code: 'invalid_bucket_name',\n message:\n 'bucketName must contain only letters, digits, hyphens, and periods; cannot start or end with punctuation or contain consecutive periods',\n }\n }\n if (name.startsWith(BUCKET_NAME_RESERVED_PREFIX)) {\n return {\n code: 'invalid_bucket_name',\n message: `bucketName cannot start with the reserved prefix \"${BUCKET_NAME_RESERVED_PREFIX}\"`,\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// File name (every upload / hide / copy entry point)\n// ---------------------------------------------------------------------------\n\nexport { FILE_NAME_MAX_BYTES }\n\n/**\n * Validates a file name against B2's documented rules. Mirrors the\n * server-side checks B2 runs on `b2_upload_file` / `b2_hide_file` /\n * `b2_start_large_file` / `b2_copy_file` filename inputs.\n *\n * @param name - Caller-supplied file name (raw, unencoded).\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-upload-file\n */\nexport function validateFileName(name: string): ValidationError | null {\n if (typeof name !== 'string' || name.length === 0) {\n return { code: 'invalid_file_name', message: 'fileName must be a non-empty string' }\n }\n const byteLength = getB2FileNameByteLength(name)\n if (byteLength === null) {\n return {\n code: 'invalid_file_name',\n message: 'fileName exceeds the 1024-byte UTF-8 limit',\n }\n }\n if (hasB2FileNameControlCharacter(name)) {\n return {\n code: 'invalid_file_name',\n message: 'fileName must not contain control characters (U+0000-U+001F or U+007F)',\n }\n }\n // Path segments equal to `.` or `..` alone are illegal per B2 docs.\n // Embedded `..` within a segment (e.g. `a..b`) is fine, and a path\n // like `../foo` is fine because the SDK doesn't interpret it; only\n // bare `.` or `..` as a complete name is rejected.\n if (name === '.' || name === '..') {\n return {\n code: 'invalid_file_name',\n message: 'fileName cannot be exactly \".\" or \"..\"',\n }\n }\n // No leading or trailing slash, no `//`.\n if (name.startsWith('/') || name.endsWith('/') || name.includes('//')) {\n return {\n code: 'invalid_file_name',\n message: 'fileName cannot start with \"/\", end with \"/\", or contain \"//\"',\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// File-info / Bucket-info byte budget\n// ---------------------------------------------------------------------------\n\n/** Per-file fileInfo total size cap (sum of key+value bytes). */\nexport const FILE_INFO_TOTAL_MAX = 2048\n/** Per-key fileInfo value byte cap. */\nexport const FILE_INFO_VALUE_MAX = 2048\n/** Allowed key character set (case-insensitive). */\nconst FILE_INFO_KEY_REGEX = /^[a-zA-Z0-9_-]+$/\n\n/**\n * Validates a `fileInfo` record against B2's byte-budget and\n * key-shape rules.\n *\n * @param info - Caller-supplied fileInfo record.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-upload-file\n */\nexport function validateFileInfo(info: Record<string, string>): ValidationError | null {\n let total = 0\n for (const [key, value] of Object.entries(info)) {\n if (!FILE_INFO_KEY_REGEX.test(key)) {\n return {\n code: 'invalid_file_info',\n message: `fileInfo key \"${key}\" must match ^[a-zA-Z0-9_-]+$`,\n }\n }\n if (typeof value !== 'string') {\n return { code: 'invalid_file_info', message: `fileInfo value for \"${key}\" must be a string` }\n }\n const keyBytes = utf8Encoder.encode(key).byteLength\n const valueBytes = utf8Encoder.encode(value).byteLength\n if (valueBytes > FILE_INFO_VALUE_MAX) {\n return {\n code: 'invalid_file_info',\n message: `fileInfo value for \"${key}\" exceeds ${FILE_INFO_VALUE_MAX} bytes`,\n }\n }\n total += keyBytes + valueBytes\n }\n if (total > FILE_INFO_TOTAL_MAX) {\n return {\n code: 'invalid_file_info',\n message: `fileInfo total size ${total} bytes exceeds the ${FILE_INFO_TOTAL_MAX}-byte limit`,\n }\n }\n return null\n}\n\n/** Per-bucket bucketInfo total max keys. */\nexport const BUCKET_INFO_MAX_KEYS = 10\n/** Per-key bucketInfo value byte cap. */\nexport const BUCKET_INFO_VALUE_MAX = 2048\n/** Allowed key character set for bucketInfo. */\nconst BUCKET_INFO_KEY_REGEX = /^[a-zA-Z0-9_-]+$/\n\n/**\n * Validates a `bucketInfo` record against B2's per-bucket metadata rules.\n *\n * @param info - Caller-supplied bucketInfo record.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-create-bucket\n */\nexport function validateBucketInfo(info: Record<string, string>): ValidationError | null {\n const entries = Object.entries(info)\n if (entries.length > BUCKET_INFO_MAX_KEYS) {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo cannot have more than ${BUCKET_INFO_MAX_KEYS} keys (got ${entries.length})`,\n }\n }\n for (const [key, value] of entries) {\n if (!BUCKET_INFO_KEY_REGEX.test(key)) {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo key \"${key}\" must match ^[a-zA-Z0-9_-]+$`,\n }\n }\n if (typeof value !== 'string') {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo value for \"${key}\" must be a string`,\n }\n }\n const valueBytes = utf8Encoder.encode(value).byteLength\n if (valueBytes > BUCKET_INFO_VALUE_MAX) {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo value for \"${key}\" exceeds ${BUCKET_INFO_VALUE_MAX} bytes`,\n }\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// maxFileCount caps per endpoint\n// ---------------------------------------------------------------------------\n\n/**\n * Per-endpoint upper bound on the `maxFileCount` (or `maxKeyCount` /\n * `maxPartCount`) request field. Sourced from the B2 endpoint docs.\n */\nexport const LIST_ENDPOINT_CAPS = {\n /** `b2_list_file_names.maxFileCount` cap. */\n b2_list_file_names: 10_000,\n /** `b2_list_file_versions.maxFileCount` cap. */\n b2_list_file_versions: 10_000,\n /** `b2_list_unfinished_large_files.maxFileCount` cap. */\n b2_list_unfinished_large_files: 100,\n /** `b2_list_keys.maxKeyCount` cap. */\n b2_list_keys: 10_000,\n /** `b2_list_parts.maxPartCount` cap. */\n b2_list_parts: 10_000,\n} as const\n\n/**\n * Validates a `maxFileCount` (or `maxKeyCount` / `maxPartCount`) value\n * against the per-endpoint cap documented by B2.\n *\n * @param requested - Caller-supplied count.\n * @param endpoint - Which list endpoint this applies to.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n */\nexport function validateMaxCount(\n requested: number | undefined,\n endpoint: keyof typeof LIST_ENDPOINT_CAPS,\n): ValidationError | null {\n if (requested === undefined) return null\n if (!Number.isInteger(requested) || requested < 1) {\n return {\n code: 'bad_request',\n message: `maxFileCount must be a positive integer (got ${requested})`,\n }\n }\n const cap = LIST_ENDPOINT_CAPS[endpoint]\n if (requested > cap) {\n return {\n code: 'bad_request',\n message: `maxFileCount ${requested} exceeds the ${endpoint} cap of ${cap}`,\n }\n }\n return null\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation.js","names":[],"sources":["../../src/simulator/validation.ts"],"sourcesContent":["/**\n * B2 spec-compliance validation helpers for the simulator.\n *\n * Each function mirrors a documented B2 input limit (file name length,\n * bucket name regex, file-info byte budget, etc.). Validators return\n * `null` for valid input or a `{ code, message }` pair that the handler\n * converts into a synthetic `400`/`403` response with the right B2\n * error code.\n *\n * Keeping validation in one module makes the simulator's \"what does B2\n * reject?\" surface auditable in a single file. Doc references inline at\n * each rule so the spec source is immediate.\n *\n * @packageDocumentation\n */\n\nimport {\n BUCKET_NAME_MAX,\n BUCKET_NAME_MIN,\n BUCKET_NAME_RESERVED_PREFIX,\n FILE_NAME_MAX_BYTES,\n getB2FileNameByteLength,\n hasB2FileNameControlCharacter,\n hasValidB2BucketNameShape,\n isB2BucketNameIpv4Address,\n} from '../internal/b2-naming.ts'\nimport { Capability } from '../types/auth.ts'\nimport { BucketRetentionMode, BucketType, CorsOperation } from '../types/bucket.ts'\nimport { EventType } from '../types/notifications.ts'\nimport { utf8Encoder } from '../util/text-codec.ts'\n\n/** Shape returned by validation functions when input is rejected. */\nexport interface ValidationError {\n readonly code: string\n readonly message: string\n}\n\n// ---------------------------------------------------------------------------\n// Application keys (`b2_create_key`)\n// ---------------------------------------------------------------------------\n\n/** Minimum length for application key names. */\nexport const KEY_NAME_MIN = 1\n/** Maximum length for application key names. */\nexport const KEY_NAME_MAX = 100\n\nconst VALID_CAPABILITIES = new Set<string>(Object.values(Capability))\n\n/**\n * Validates application-key capabilities against B2's supported capability set.\n *\n * @param capabilities - Caller-supplied capabilities array.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-create-key\n */\nexport function validateCreateKeyCapabilities(capabilities: unknown): ValidationError | null {\n if (!Array.isArray(capabilities)) {\n return { code: 'bad_request', message: 'capabilities must be an array' }\n }\n if (capabilities.length === 0) {\n return { code: 'bad_request', message: 'capabilities must not be empty' }\n }\n const unknownCapabilities = capabilities.filter(\n (capability) => typeof capability !== 'string' || !VALID_CAPABILITIES.has(capability),\n )\n if (unknownCapabilities.length > 0) {\n return {\n code: 'bad_request',\n message: `unknown capabilities: ${unknownCapabilities.map(String).join(', ')}`,\n }\n }\n return null\n}\n\n/**\n * Returns a frozen copy of a previously validated application-key capability list.\n *\n * @param capabilities - Validated caller-supplied capabilities.\n *\n * @returns An immutable capability list safe to store internally.\n */\nexport function normalizeCreateKeyCapabilities(\n capabilities: readonly Capability[],\n): readonly Capability[] {\n return Object.freeze([...capabilities])\n}\n\n/**\n * Validates application-key names against B2's documented length limits.\n *\n * @param keyName - Caller-supplied keyName.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-create-key\n */\nexport function validateCreateKeyName(keyName: unknown): ValidationError | null {\n if (typeof keyName !== 'string') {\n return { code: 'bad_request', message: 'keyName must be a string' }\n }\n if (keyName.length < KEY_NAME_MIN || keyName.length > KEY_NAME_MAX) {\n return {\n code: 'bad_request',\n message: `keyName must be ${KEY_NAME_MIN}-${KEY_NAME_MAX} characters`,\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Bucket name (`b2_create_bucket`, `b2_update_bucket`)\n// ---------------------------------------------------------------------------\n\nexport { BUCKET_NAME_MAX, BUCKET_NAME_MIN }\n\n/**\n * Validates a bucket name against B2's documented rules.\n *\n * @param name - Caller-supplied bucket name.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-create-bucket\n */\nexport function validateBucketName(name: string): ValidationError | null {\n if (typeof name !== 'string' || name.length < BUCKET_NAME_MIN || name.length > BUCKET_NAME_MAX) {\n return {\n code: 'invalid_bucket_name',\n message: `bucketName must be ${BUCKET_NAME_MIN}-${BUCKET_NAME_MAX} characters`,\n }\n }\n if (isB2BucketNameIpv4Address(name)) {\n return {\n code: 'invalid_bucket_name',\n message: 'bucketName must not be formatted as an IPv4 address',\n }\n }\n if (!hasValidB2BucketNameShape(name)) {\n return {\n code: 'invalid_bucket_name',\n message:\n 'bucketName must contain only letters, digits, hyphens, and periods; cannot start or end with punctuation or contain consecutive periods',\n }\n }\n if (name.startsWith(BUCKET_NAME_RESERVED_PREFIX)) {\n return {\n code: 'invalid_bucket_name',\n message: `bucketName cannot start with the reserved prefix \"${BUCKET_NAME_RESERVED_PREFIX}\"`,\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// File name (every upload / hide / copy entry point)\n// ---------------------------------------------------------------------------\n\nexport { FILE_NAME_MAX_BYTES }\n\n/**\n * Validates a file name against B2's documented rules. Mirrors the\n * server-side checks B2 runs on `b2_upload_file` / `b2_hide_file` /\n * `b2_start_large_file` / `b2_copy_file` filename inputs.\n *\n * @param name - Caller-supplied file name (raw, unencoded).\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-upload-file\n */\nexport function validateFileName(name: string): ValidationError | null {\n if (typeof name !== 'string' || name.length === 0) {\n return { code: 'invalid_file_name', message: 'fileName must be a non-empty string' }\n }\n const byteLength = getB2FileNameByteLength(name)\n if (byteLength === null) {\n return {\n code: 'invalid_file_name',\n message: 'fileName exceeds the 1024-byte UTF-8 limit',\n }\n }\n if (hasB2FileNameControlCharacter(name)) {\n return {\n code: 'invalid_file_name',\n message: 'fileName must not contain control characters (U+0000-U+001F or U+007F)',\n }\n }\n // Path segments equal to `.` or `..` alone are illegal per B2 docs.\n // Embedded `..` within a segment (e.g. `a..b`) is fine, and a path\n // like `../foo` is fine because the SDK doesn't interpret it; only\n // bare `.` or `..` as a complete name is rejected.\n if (name === '.' || name === '..') {\n return {\n code: 'invalid_file_name',\n message: 'fileName cannot be exactly \".\" or \"..\"',\n }\n }\n // No leading or trailing slash, no `//`.\n if (name.startsWith('/') || name.endsWith('/') || name.includes('//')) {\n return {\n code: 'invalid_file_name',\n message: 'fileName cannot start with \"/\", end with \"/\", or contain \"//\"',\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// File-info / Bucket-info byte budget\n// ---------------------------------------------------------------------------\n\n/** Per-file fileInfo total size cap (sum of key+value bytes). */\nexport const FILE_INFO_TOTAL_MAX = 2048\n/** Per-key fileInfo value byte cap. */\nexport const FILE_INFO_VALUE_MAX = 2048\n/** Allowed key character set (case-insensitive). */\nconst FILE_INFO_KEY_REGEX = /^[a-zA-Z0-9_-]+$/\n\n/**\n * Validates a `fileInfo` record against B2's byte-budget and\n * key-shape rules.\n *\n * @param info - Caller-supplied fileInfo record.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-upload-file\n */\nexport function validateFileInfo(info: Record<string, string>): ValidationError | null {\n let total = 0\n for (const [key, value] of Object.entries(info)) {\n if (!FILE_INFO_KEY_REGEX.test(key)) {\n return {\n code: 'invalid_file_info',\n message: `fileInfo key \"${key}\" must match ^[a-zA-Z0-9_-]+$`,\n }\n }\n if (typeof value !== 'string') {\n return { code: 'invalid_file_info', message: `fileInfo value for \"${key}\" must be a string` }\n }\n const keyBytes = utf8Encoder.encode(key).byteLength\n const valueBytes = utf8Encoder.encode(value).byteLength\n if (valueBytes > FILE_INFO_VALUE_MAX) {\n return {\n code: 'invalid_file_info',\n message: `fileInfo value for \"${key}\" exceeds ${FILE_INFO_VALUE_MAX} bytes`,\n }\n }\n total += keyBytes + valueBytes\n }\n if (total > FILE_INFO_TOTAL_MAX) {\n return {\n code: 'invalid_file_info',\n message: `fileInfo total size ${total} bytes exceeds the ${FILE_INFO_TOTAL_MAX}-byte limit`,\n }\n }\n return null\n}\n\n/** Per-bucket bucketInfo total max keys. */\nexport const BUCKET_INFO_MAX_KEYS = 10\n/** Per-key bucketInfo value byte cap. */\nexport const BUCKET_INFO_VALUE_MAX = 2048\n/** Allowed key character set for bucketInfo. */\nconst BUCKET_INFO_KEY_REGEX = /^[a-zA-Z0-9_-]+$/\n\n/**\n * Validates a `bucketInfo` record against B2's per-bucket metadata rules.\n *\n * @param info - Caller-supplied bucketInfo record.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-create-bucket\n */\nexport function validateBucketInfo(info: Record<string, string>): ValidationError | null {\n const entries = Object.entries(info)\n if (entries.length > BUCKET_INFO_MAX_KEYS) {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo cannot have more than ${BUCKET_INFO_MAX_KEYS} keys (got ${entries.length})`,\n }\n }\n for (const [key, value] of entries) {\n if (!BUCKET_INFO_KEY_REGEX.test(key)) {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo key \"${key}\" must match ^[a-zA-Z0-9_-]+$`,\n }\n }\n if (typeof value !== 'string') {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo value for \"${key}\" must be a string`,\n }\n }\n const valueBytes = utf8Encoder.encode(value).byteLength\n if (valueBytes > BUCKET_INFO_VALUE_MAX) {\n return {\n code: 'invalid_bucket_info',\n message: `bucketInfo value for \"${key}\" exceeds ${BUCKET_INFO_VALUE_MAX} bytes`,\n }\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// maxFileCount caps per endpoint\n// ---------------------------------------------------------------------------\n\n/**\n * Per-endpoint upper bound on the `maxFileCount` (or `maxKeyCount` /\n * `maxPartCount`) request field. Sourced from the B2 endpoint docs.\n */\nexport const LIST_ENDPOINT_CAPS = {\n /** `b2_list_file_names.maxFileCount` cap. */\n b2_list_file_names: 10_000,\n /** `b2_list_file_versions.maxFileCount` cap. */\n b2_list_file_versions: 10_000,\n /** `b2_list_unfinished_large_files.maxFileCount` cap. */\n b2_list_unfinished_large_files: 100,\n /** `b2_list_keys.maxKeyCount` cap. */\n b2_list_keys: 10_000,\n /** `b2_list_parts.maxPartCount` cap. */\n b2_list_parts: 10_000,\n} as const\n\n/**\n * Validates a `maxFileCount` (or `maxKeyCount` / `maxPartCount`) value\n * against the per-endpoint cap documented by B2.\n *\n * @param requested - Caller-supplied count.\n * @param endpoint - Which list endpoint this applies to.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n */\nexport function validateMaxCount(\n requested: number | undefined,\n endpoint: keyof typeof LIST_ENDPOINT_CAPS,\n): ValidationError | null {\n if (requested === undefined) return null\n if (!Number.isInteger(requested) || requested < 1) {\n return {\n code: 'bad_request',\n message: `maxFileCount must be a positive integer (got ${requested})`,\n }\n }\n const cap = LIST_ENDPOINT_CAPS[endpoint]\n if (requested > cap) {\n return {\n code: 'bad_request',\n message: `maxFileCount ${requested} exceeds the ${endpoint} cap of ${cap}`,\n }\n }\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Download authorization duration\n// ---------------------------------------------------------------------------\n\n/** Minimum `b2_get_download_authorization.validDurationInSeconds`. */\nexport const DOWNLOAD_AUTH_DURATION_MIN_SECONDS = 1\n/** Maximum `b2_get_download_authorization.validDurationInSeconds`. */\nexport const DOWNLOAD_AUTH_DURATION_MAX_SECONDS = 604_800\n\n/**\n * Validates a `b2_get_download_authorization` duration against B2's\n * documented inclusive 1-second to 7-day range.\n *\n * @param requested - Caller-supplied duration in seconds.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n */\nexport function validateDownloadAuthorizationDuration(requested: unknown): ValidationError | null {\n if (\n typeof requested !== 'number' ||\n !Number.isInteger(requested) ||\n requested < DOWNLOAD_AUTH_DURATION_MIN_SECONDS ||\n requested > DOWNLOAD_AUTH_DURATION_MAX_SECONDS\n ) {\n return {\n code: 'bad_request',\n message: `validDurationInSeconds must be an integer from ${DOWNLOAD_AUTH_DURATION_MIN_SECONDS} through ${DOWNLOAD_AUTH_DURATION_MAX_SECONDS}`,\n }\n }\n return null\n}\n\n/**\n * Validates a `b2_get_download_authorization` file name prefix.\n *\n * B2 allows an empty prefix, but the field must still be present as a\n * string so later prefix checks cannot crash on malformed request bodies.\n *\n * @param requested - Caller-supplied file name prefix.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n */\nexport function validateDownloadAuthorizationPrefix(requested: unknown): ValidationError | null {\n if (typeof requested !== 'string') {\n return { code: 'bad_request', message: 'fileNamePrefix must be a string' }\n }\n return null\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nfunction validateKnownFields(\n value: Record<string, unknown>,\n allowedFields: ReadonlySet<string>,\n path: string,\n): ValidationError | null {\n for (const field of Object.keys(value)) {\n if (!allowedFields.has(field)) {\n return {\n code: 'bad_request',\n message: `${path}.${field} is not a supported field`,\n }\n }\n }\n return null\n}\n\nfunction validateStringArray(\n value: unknown,\n path: string,\n options: { readonly allowNull?: boolean; readonly requireNonEmpty?: boolean } = {},\n): ValidationError | null {\n if (value === null && options.allowNull === true) return null\n if (!Array.isArray(value)) {\n return { code: 'bad_request', message: `${path} must be an array of strings` }\n }\n if (options.requireNonEmpty === true && value.length === 0) {\n return { code: 'bad_request', message: `${path} must be a non-empty array` }\n }\n for (const [index, item] of value.entries()) {\n if (typeof item !== 'string') {\n return { code: 'bad_request', message: `${path}[${index}] must be a string` }\n }\n }\n return null\n}\n\nfunction validateNonEmptyString(value: unknown, path: string): ValidationError | null {\n if (typeof value !== 'string' || value.length === 0) {\n return { code: 'bad_request', message: `${path} must be a non-empty string` }\n }\n return null\n}\n\nfunction validateNonNegativeInteger(value: unknown, path: string): ValidationError | null {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return { code: 'bad_request', message: `${path} must be a non-negative integer` }\n }\n return null\n}\n\nfunction validateIntegerAtMost(value: number, max: number, path: string): ValidationError | null {\n if (value > max) {\n return { code: 'bad_request', message: `${path} must not exceed ${max}` }\n }\n return null\n}\n\nfunction validatePositiveInteger(value: unknown, path: string): ValidationError | null {\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {\n return { code: 'bad_request', message: `${path} must be a positive integer` }\n }\n return null\n}\n\nfunction validateNullablePositiveInteger(value: unknown, path: string): ValidationError | null {\n if (value === undefined || value === null) return null\n return validatePositiveInteger(value, path)\n}\n\nfunction validateUniqueName(\n names: Set<string>,\n name: string,\n path: string,\n): ValidationError | null {\n if (names.has(name)) {\n return { code: 'bad_request', message: `${path} must be unique` }\n }\n names.add(name)\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Bucket configuration (`b2_create_bucket`, `b2_update_bucket`)\n// ---------------------------------------------------------------------------\n\nconst KNOWN_BUCKET_TYPES = new Set<string>(Object.values(BucketType))\n\n/**\n * Validates a `b2_list_buckets.bucketTypes` filter.\n *\n * @param bucketTypes - Caller-supplied bucket type filter.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-list-buckets\n */\nexport function validateBucketTypes(bucketTypes: unknown): ValidationError | null {\n if (bucketTypes === undefined) return null\n if (!Array.isArray(bucketTypes)) {\n return { code: 'bad_request', message: 'bucketTypes must be an array' }\n }\n for (const [index, bucketType] of bucketTypes.entries()) {\n if (typeof bucketType !== 'string' || !KNOWN_BUCKET_TYPES.has(bucketType)) {\n return {\n code: 'bad_request',\n message: `bucketTypes[${index}] must be a known bucket type`,\n }\n }\n }\n return null\n}\n\nconst CORS_RULE_FIELDS = new Set([\n 'allowedHeaders',\n 'allowedOperations',\n 'allowedOrigins',\n 'corsRuleName',\n 'exposeHeaders',\n 'maxAgeSeconds',\n])\n\nconst CORS_MAX_AGE_SECONDS_MAX = 86_400\nconst KNOWN_CORS_OPERATIONS = new Set<string>(Object.values(CorsOperation))\n\n/**\n * Validates B2 CORS rule structure for bucket create/update requests.\n *\n * @param rules - Caller-supplied CORS rules.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules\n */\nexport function validateCorsRules(rules: unknown): ValidationError | null {\n if (!Array.isArray(rules)) {\n return { code: 'bad_request', message: 'corsRules must be an array' }\n }\n\n const names = new Set<string>()\n for (const [index, rule] of rules.entries()) {\n const rulePath = `corsRules[${index}]`\n if (!isRecord(rule)) {\n return { code: 'bad_request', message: `${rulePath} must be an object` }\n }\n\n const fieldError = validateKnownFields(rule, CORS_RULE_FIELDS, rulePath)\n if (fieldError) return fieldError\n\n const nameError = validateNonEmptyString(rule['corsRuleName'], `${rulePath}.corsRuleName`)\n if (nameError) return nameError\n const ruleName = rule['corsRuleName'] as string\n const uniqueNameError = validateUniqueName(names, ruleName, `${rulePath}.corsRuleName`)\n if (uniqueNameError) return uniqueNameError\n\n const originsError = validateStringArray(rule['allowedOrigins'], `${rulePath}.allowedOrigins`, {\n requireNonEmpty: true,\n })\n if (originsError) return originsError\n\n const operationsError = validateStringArray(\n rule['allowedOperations'],\n `${rulePath}.allowedOperations`,\n { requireNonEmpty: true },\n )\n if (operationsError) return operationsError\n for (const operation of rule['allowedOperations'] as readonly string[]) {\n if (!KNOWN_CORS_OPERATIONS.has(operation)) {\n return {\n code: 'bad_request',\n message: `${rulePath}.allowedOperations contains an unknown operation`,\n }\n }\n }\n\n const headersError = validateStringArray(rule['allowedHeaders'], `${rulePath}.allowedHeaders`, {\n allowNull: true,\n })\n if (headersError) return headersError\n\n const exposeError = validateStringArray(rule['exposeHeaders'], `${rulePath}.exposeHeaders`, {\n allowNull: true,\n })\n if (exposeError) return exposeError\n\n const ageError = validateNonNegativeInteger(rule['maxAgeSeconds'], `${rulePath}.maxAgeSeconds`)\n if (ageError) return ageError\n const maxAgeError = validateIntegerAtMost(\n rule['maxAgeSeconds'] as number,\n CORS_MAX_AGE_SECONDS_MAX,\n `${rulePath}.maxAgeSeconds`,\n )\n if (maxAgeError) return maxAgeError\n }\n\n return null\n}\n\nconst LIFECYCLE_RULE_FIELDS = new Set([\n 'daysFromHidingToDeleting',\n 'daysFromStartingToCancelingUnfinishedLargeFiles',\n 'daysFromUploadingToHiding',\n 'fileNamePrefix',\n])\n\n/**\n * Validates B2 lifecycle rule structure for bucket create/update requests.\n *\n * @param rules - Caller-supplied lifecycle rules.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/docs/cloud-storage-lifecycle-rules\n */\nexport function validateLifecycleRules(rules: unknown): ValidationError | null {\n if (!Array.isArray(rules)) {\n return { code: 'bad_request', message: 'lifecycleRules must be an array' }\n }\n\n for (const [index, rule] of rules.entries()) {\n const rulePath = `lifecycleRules[${index}]`\n if (!isRecord(rule)) {\n return { code: 'bad_request', message: `${rulePath} must be an object` }\n }\n\n const fieldError = validateKnownFields(rule, LIFECYCLE_RULE_FIELDS, rulePath)\n if (fieldError) return fieldError\n\n const deletingError = validateNullablePositiveInteger(\n rule['daysFromHidingToDeleting'],\n `${rulePath}.daysFromHidingToDeleting`,\n )\n if (deletingError) return deletingError\n\n const hidingError = validateNullablePositiveInteger(\n rule['daysFromUploadingToHiding'],\n `${rulePath}.daysFromUploadingToHiding`,\n )\n if (hidingError) return hidingError\n\n const cancelError = validateNullablePositiveInteger(\n rule['daysFromStartingToCancelingUnfinishedLargeFiles'],\n `${rulePath}.daysFromStartingToCancelingUnfinishedLargeFiles`,\n )\n if (cancelError) return cancelError\n\n if (\n rule['daysFromHidingToDeleting'] == null &&\n rule['daysFromUploadingToHiding'] == null &&\n rule['daysFromStartingToCancelingUnfinishedLargeFiles'] == null\n ) {\n return {\n code: 'bad_request',\n message: `${rulePath} must set at least one lifecycle action`,\n }\n }\n\n if (typeof rule['fileNamePrefix'] !== 'string') {\n return { code: 'bad_request', message: `${rulePath}.fileNamePrefix must be a string` }\n }\n }\n\n return null\n}\n\nconst DEFAULT_RETENTION_FIELDS = new Set(['mode', 'period'])\nconst RETENTION_PERIOD_FIELDS = new Set(['duration', 'unit'])\nconst DEFAULT_RETENTION_MODES = new Set<string>(Object.values(BucketRetentionMode))\nconst OBJECT_LOCK_RETENTION_MAX_DAYS = 3000\n\n/**\n * Validates default Object Lock retention policy structure for bucket requests.\n *\n * @param policy - Caller-supplied default retention policy.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/docs/cloud-storage-object-lock\n */\nexport function validateDefaultRetention(policy: unknown): ValidationError | null {\n if (!isRecord(policy)) {\n return { code: 'bad_request', message: 'defaultRetention must be an object' }\n }\n\n const fieldError = validateKnownFields(policy, DEFAULT_RETENTION_FIELDS, 'defaultRetention')\n if (fieldError) return fieldError\n\n const mode = policy['mode']\n if (typeof mode !== 'string' || !DEFAULT_RETENTION_MODES.has(mode)) {\n return {\n code: 'bad_request',\n message: 'defaultRetention.mode must be \"compliance\", \"governance\", or \"none\"',\n }\n }\n\n const period = policy['period']\n if (mode === BucketRetentionMode.None) {\n if (period !== null) {\n return {\n code: 'bad_request',\n message: 'defaultRetention.period must be null when mode is \"none\"',\n }\n }\n return null\n }\n\n if (!isRecord(period)) {\n return {\n code: 'bad_request',\n message: 'defaultRetention.period must be an object for retention modes',\n }\n }\n\n const periodFieldError = validateKnownFields(\n period,\n RETENTION_PERIOD_FIELDS,\n 'defaultRetention.period',\n )\n if (periodFieldError) return periodFieldError\n\n const durationError = validatePositiveInteger(\n period['duration'],\n 'defaultRetention.period.duration',\n )\n if (durationError) return durationError\n if (!Number.isSafeInteger(period['duration'])) {\n return {\n code: 'bad_request',\n message: 'defaultRetention.period.duration must be a safe integer',\n }\n }\n\n if (period['unit'] !== 'days' && period['unit'] !== 'years') {\n return {\n code: 'bad_request',\n message: 'defaultRetention.period.unit must be \"days\" or \"years\"',\n }\n }\n const durationDays =\n period['unit'] === 'days'\n ? (period['duration'] as number)\n : (period['duration'] as number) * 365\n if (!Number.isFinite(durationDays) || durationDays > OBJECT_LOCK_RETENTION_MAX_DAYS) {\n return {\n code: 'bad_request',\n message: `defaultRetention.period must not exceed ${OBJECT_LOCK_RETENTION_MAX_DAYS} days`,\n }\n }\n\n return null\n}\n\nconst REPLICATION_CONFIG_FIELDS = new Set(['asReplicationSource', 'asReplicationDestination'])\nconst REPLICATION_SOURCE_FIELDS = new Set(['replicationRules', 'sourceApplicationKeyId'])\nconst REPLICATION_DESTINATION_FIELDS = new Set(['sourceToDestinationKeyMapping'])\nconst REPLICATION_RULE_FIELDS = new Set([\n 'destinationBucketId',\n 'fileNamePrefix',\n 'includeExistingFiles',\n 'isEnabled',\n 'priority',\n 'replicationRuleName',\n])\n\nfunction validateReplicationRule(rule: unknown, rulePath: string): ValidationError | null {\n if (!isRecord(rule)) {\n return { code: 'bad_request', message: `${rulePath} must be an object` }\n }\n\n const fieldError = validateKnownFields(rule, REPLICATION_RULE_FIELDS, rulePath)\n if (fieldError) return fieldError\n\n const destinationError = validateNonEmptyString(\n rule['destinationBucketId'],\n `${rulePath}.destinationBucketId`,\n )\n if (destinationError) return destinationError\n\n if (typeof rule['fileNamePrefix'] !== 'string') {\n return { code: 'bad_request', message: `${rulePath}.fileNamePrefix must be a string` }\n }\n\n if (typeof rule['includeExistingFiles'] !== 'boolean') {\n return { code: 'bad_request', message: `${rulePath}.includeExistingFiles must be a boolean` }\n }\n\n if (typeof rule['isEnabled'] !== 'boolean') {\n return { code: 'bad_request', message: `${rulePath}.isEnabled must be a boolean` }\n }\n\n const priorityError = validatePositiveInteger(rule['priority'], `${rulePath}.priority`)\n if (priorityError) return priorityError\n\n const nameError = validateNonEmptyString(\n rule['replicationRuleName'],\n `${rulePath}.replicationRuleName`,\n )\n if (nameError) return nameError\n\n return null\n}\n\nfunction validateReplicationSource(source: unknown): ValidationError | null {\n if (!isRecord(source)) {\n return {\n code: 'bad_request',\n message: 'replicationConfiguration.asReplicationSource must be an object',\n }\n }\n\n const fieldError = validateKnownFields(\n source,\n REPLICATION_SOURCE_FIELDS,\n 'replicationConfiguration.asReplicationSource',\n )\n if (fieldError) return fieldError\n\n const sourceKeyError = validateNonEmptyString(\n source['sourceApplicationKeyId'],\n 'replicationConfiguration.asReplicationSource.sourceApplicationKeyId',\n )\n if (sourceKeyError) return sourceKeyError\n\n const rules = source['replicationRules']\n if (!Array.isArray(rules)) {\n return {\n code: 'bad_request',\n message: 'replicationConfiguration.asReplicationSource.replicationRules must be an array',\n }\n }\n\n const names = new Set<string>()\n for (const [index, rule] of rules.entries()) {\n const rulePath = `replicationConfiguration.asReplicationSource.replicationRules[${index}]`\n const ruleError = validateReplicationRule(rule, rulePath)\n if (ruleError) return ruleError\n const ruleName = (rule as Record<string, unknown>)['replicationRuleName'] as string\n const uniqueNameError = validateUniqueName(names, ruleName, `${rulePath}.replicationRuleName`)\n if (uniqueNameError) return uniqueNameError\n }\n\n return null\n}\n\nfunction validateReplicationDestination(destination: unknown): ValidationError | null {\n if (!isRecord(destination)) {\n return {\n code: 'bad_request',\n message: 'replicationConfiguration.asReplicationDestination must be an object',\n }\n }\n\n const fieldError = validateKnownFields(\n destination,\n REPLICATION_DESTINATION_FIELDS,\n 'replicationConfiguration.asReplicationDestination',\n )\n if (fieldError) return fieldError\n\n const mapping = destination['sourceToDestinationKeyMapping']\n if (!isRecord(mapping)) {\n return {\n code: 'bad_request',\n message:\n 'replicationConfiguration.asReplicationDestination.sourceToDestinationKeyMapping must be an object',\n }\n }\n\n for (const [sourceKey, destinationKey] of Object.entries(mapping)) {\n if (sourceKey.length === 0) {\n return {\n code: 'bad_request',\n message:\n 'replicationConfiguration.asReplicationDestination.sourceToDestinationKeyMapping keys must be non-empty',\n }\n }\n if (typeof destinationKey !== 'string' || destinationKey.length === 0) {\n return {\n code: 'bad_request',\n message:\n 'replicationConfiguration.asReplicationDestination.sourceToDestinationKeyMapping values must be non-empty strings',\n }\n }\n }\n\n return null\n}\n\n/**\n * Validates B2 replication configuration structure for bucket requests.\n *\n * @param config - Caller-supplied replication configuration.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-update-bucket\n */\nexport function validateReplicationConfiguration(config: unknown): ValidationError | null {\n if (!isRecord(config)) {\n return { code: 'bad_request', message: 'replicationConfiguration must be an object' }\n }\n\n const fieldError = validateKnownFields(\n config,\n REPLICATION_CONFIG_FIELDS,\n 'replicationConfiguration',\n )\n if (fieldError) return fieldError\n\n if (!('asReplicationSource' in config) && !('asReplicationDestination' in config)) {\n return {\n code: 'bad_request',\n message:\n 'replicationConfiguration must include asReplicationSource or asReplicationDestination',\n }\n }\n\n const source = config['asReplicationSource']\n if (source !== undefined && source !== null) {\n const sourceError = validateReplicationSource(source)\n if (sourceError) return sourceError\n }\n\n const destination = config['asReplicationDestination']\n if (destination !== undefined && destination !== null) {\n const destinationError = validateReplicationDestination(destination)\n if (destinationError) return destinationError\n }\n\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Event notification rules (`b2_set_bucket_notification_rules`)\n// ---------------------------------------------------------------------------\n\nconst NOTIFICATION_RULE_FIELDS = new Set([\n 'eventTypes',\n 'isEnabled',\n 'isSuspended',\n 'maxEventsPerBatch',\n 'name',\n 'objectNamePrefix',\n 'suspensionReason',\n 'targetConfiguration',\n])\n\nconst NOTIFICATION_TARGET_FIELDS = new Set([\n 'customHeaders',\n 'hmacSha256SigningSecret',\n 'targetType',\n 'url',\n])\n\nconst KNOWN_NOTIFICATION_EVENT_TYPES = new Set<string>(Object.values(EventType))\n\n/**\n * Validates event notification rules for `b2_set_bucket_notification_rules`.\n *\n * @param rules - Caller-supplied event notification rules.\n *\n * @returns A `{ code, message }` pair on failure, or `null` when valid.\n *\n * @see https://www.backblaze.com/apidocs/b2-set-bucket-notification-rules\n */\nexport function validateNotificationRules(rules: unknown): ValidationError | null {\n if (!Array.isArray(rules)) {\n return {\n code: 'bad_request',\n message: 'eventNotificationRules must be an array',\n }\n }\n\n const names = new Set<string>()\n for (const [index, ruleValue] of rules.entries()) {\n const rulePath = `eventNotificationRules[${index}]`\n if (!isRecord(ruleValue)) {\n return { code: 'bad_request', message: `${rulePath} must be an object` }\n }\n\n const fieldError = validateKnownFields(ruleValue, NOTIFICATION_RULE_FIELDS, rulePath)\n if (fieldError) return fieldError\n\n if (typeof ruleValue['isEnabled'] !== 'boolean') {\n return {\n code: 'bad_request',\n message: `${rulePath}.isEnabled must be a boolean`,\n }\n }\n\n const maxEventsPerBatch = ruleValue['maxEventsPerBatch']\n if (\n maxEventsPerBatch !== undefined &&\n (typeof maxEventsPerBatch !== 'number' ||\n !Number.isInteger(maxEventsPerBatch) ||\n maxEventsPerBatch < 1)\n ) {\n return {\n code: 'bad_request',\n message: `${rulePath}.maxEventsPerBatch must be a positive integer`,\n }\n }\n\n const name = ruleValue['name']\n if (typeof name !== 'string' || name.length === 0) {\n return {\n code: 'bad_request',\n message: `${rulePath}.name must be a non-empty string`,\n }\n }\n if (names.has(name)) {\n return {\n code: 'bad_request',\n message: `${rulePath}.name must be unique within eventNotificationRules`,\n }\n }\n names.add(name)\n\n const eventTypes = ruleValue['eventTypes']\n if (!Array.isArray(eventTypes) || eventTypes.length === 0) {\n return {\n code: 'bad_request',\n message: `${rulePath}.eventTypes must be a non-empty array`,\n }\n }\n for (const eventType of eventTypes) {\n if (typeof eventType !== 'string' || !KNOWN_NOTIFICATION_EVENT_TYPES.has(eventType)) {\n return {\n code: 'bad_request',\n message: `${rulePath}.eventTypes contains an unknown event type`,\n }\n }\n }\n\n const targetConfiguration = ruleValue['targetConfiguration']\n if (!isRecord(targetConfiguration)) {\n return {\n code: 'bad_request',\n message: `${rulePath}.targetConfiguration must be an object`,\n }\n }\n\n const targetFieldError = validateKnownFields(\n targetConfiguration,\n NOTIFICATION_TARGET_FIELDS,\n `${rulePath}.targetConfiguration`,\n )\n if (targetFieldError) return targetFieldError\n\n if (targetConfiguration['targetType'] !== 'webhook') {\n return {\n code: 'bad_request',\n message: `${rulePath}.targetConfiguration.targetType must be \"webhook\"`,\n }\n }\n\n const url = targetConfiguration['url']\n if (typeof url !== 'string') {\n return {\n code: 'bad_request',\n message: `${rulePath}.targetConfiguration.url must be a valid https URL`,\n }\n }\n try {\n if (new URL(url).protocol !== 'https:') {\n return {\n code: 'bad_request',\n message: `${rulePath}.targetConfiguration.url must be a valid https URL`,\n }\n }\n } catch {\n return {\n code: 'bad_request',\n message: `${rulePath}.targetConfiguration.url must be a valid https URL`,\n }\n }\n }\n\n return null\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-source.cjs","names":[],"sources":["../../src/streams/file-source.ts"],"sourcesContent":["import type { ContentSource } from './source.ts'\n\nconst FILE_STREAM_CHUNK_SIZE = 16 * 1024 * 1024\n\n/** Filesystem path accepted by {@link FileSource}. */\nexport type FileSourcePath = string | URL\n\ninterface FileStatsLike {\n readonly dev: number\n readonly ino: number\n readonly mode: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n isFile(): boolean\n}\n\ninterface FileHandleLike {\n read(\n buffer: Uint8Array,\n offset: number,\n length: number,\n position: number,\n ): Promise<{ bytesRead: number }>\n stat(): Promise<FileStatsLike>\n close(): Promise<void>\n}\n\ninterface NodeFsSync {\n lstatSync(path: FileSourcePath): FileStatsLike\n}\n\ninterface FileIdentity {\n readonly dev: number\n readonly ino: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n}\n\ninterface ToArrayBufferOptions {\n readonly signal?: AbortSignal\n}\n\nconst FILE_SOURCE_INTERNAL = Symbol('FileSource.internal')\n\ninterface FileSourceInternalOptions {\n readonly key: typeof FILE_SOURCE_INTERNAL\n readonly identity: FileIdentity\n}\n\n/** @internal */\nexport const fileSourceTestHooks: {\n afterReadIteration?: (filled: number) => Promise<void> | void\n maxReadSize?: number\n openFile?: (path: FileSourcePath, flags: number) => Promise<FileHandleLike>\n platform?: string\n} = {}\n\nfunction getNodeFsSync(): NodeFsSync {\n // FileSource exposes `size` synchronously, so the constructor cannot use the\n // repo's usual `await import('node:...')` pattern for Node-only APIs. Keep the\n // synchronous lookup here; async callers should prefer `FileSource.fromPath()`.\n const processLike = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown }\n }\n ).process\n const fs = processLike?.getBuiltinModule?.('node:fs')\n if (!isNodeFsSync(fs)) {\n throw new Error(\n 'FileSource constructor requires Node.js 22.3+ synchronous filesystem APIs; use FileSource.fromPath() when synchronous filesystem access is unavailable.',\n )\n }\n return fs\n}\n\nfunction isNodeFsSync(value: unknown): value is NodeFsSync {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Record<string, unknown>\n return typeof candidate['lstatSync'] === 'function'\n}\n\nasync function fileOpenFlags(): Promise<number> {\n const { constants } = await import('node:fs')\n // O_NOFOLLOW and O_NONBLOCK are unavailable on some platforms. When\n // O_NOFOLLOW is absent, the post-open fstat identity check below is the\n // symlink-swap defense. O_NONBLOCK avoids blocking indefinitely if a path is\n // swapped to a FIFO or another blocking special file before fstat rejects it.\n return (\n constants.O_RDONLY |\n (constants.O_NOFOLLOW ?? 0) |\n ((constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0)\n )\n}\n\nfunction normalizeSliceOffset(value: number, size: number): number {\n if (!Number.isFinite(value)) throw new RangeError('FileSource slice offsets must be finite.')\n const integer = Math.trunc(value)\n const offset = integer < 0 ? size + integer : integer\n return Math.min(Math.max(offset, 0), size)\n}\n\nfunction formatFilePath(path: FileSourcePath): string {\n return path instanceof URL ? path.href : path\n}\n\nfunction identityFromStats(stats: FileStatsLike): FileIdentity {\n return {\n dev: stats.dev,\n ino: stats.ino,\n size: stats.size,\n mtimeMs: stats.mtimeMs,\n ctimeMs: stats.ctimeMs,\n }\n}\n\nfunction assertStableIdentity(path: FileSourcePath, stats: FileStatsLike): void {\n if (stats.dev === 0 && stats.ino === 0) {\n throw new Error(\n `FileSource: ${formatFilePath(path)} is on a filesystem that does not expose stable file identity.`,\n )\n }\n}\n\nfunction assertRegularFile(path: FileSourcePath, stats: FileStatsLike): void {\n if (!stats.isFile()) {\n throw new Error(`FileSource: ${formatFilePath(path)} is not a regular file.`)\n }\n}\n\nfunction validatedIdentityFromStats(path: FileSourcePath, stats: FileStatsLike): FileIdentity {\n assertRegularFile(path, stats)\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, stats)\n return identityFromStats(stats)\n}\n\nfunction assertSameIdentity(\n path: FileSourcePath,\n expected: FileIdentity,\n actual: FileStatsLike,\n when: string,\n): void {\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, actual)\n if (\n shouldComparePosixFileIdentity() &&\n (actual.dev !== expected.dev || actual.ino !== expected.ino)\n ) {\n throw new Error(`FileSource: ${formatFilePath(path)} changed ${when}.`)\n }\n if (actual.size !== expected.size || actual.mtimeMs !== expected.mtimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n if (shouldComparePosixChangeTime() && actual.ctimeMs !== expected.ctimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n}\n\nfunction isWindows(): boolean {\n if (fileSourceTestHooks.platform !== undefined) return fileSourceTestHooks.platform === 'win32'\n const processLike = (globalThis as { process?: { platform?: string } }).process\n return processLike?.platform === 'win32'\n}\n\nfunction shouldComparePosixFileIdentity(): boolean {\n return !isWindows()\n}\n\nfunction shouldComparePosixChangeTime(): boolean {\n return !isWindows()\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal === undefined || !signal.aborted) return\n throw signal.reason ?? new DOMException('Aborted', 'AbortError')\n}\n\nfunction maxReadSize(): number {\n return fileSourceTestHooks.maxReadSize ?? FILE_STREAM_CHUNK_SIZE\n}\n\n/* v8 ignore start -- Requires a file to pass identity checks and still EOF mid-range. */\nfunction rangeEndedEarlyError(path: FileSourcePath, offset: number, size: number): Error {\n const end = offset + size\n return new Error(\n `FileSource: ${formatFilePath(path)} ended before byte range [${offset}, ${end}) was fully read.`,\n )\n}\n/* v8 ignore stop */\n\nasync function openValidatedFile(\n path: FileSourcePath,\n identity: FileIdentity,\n): Promise<FileHandleLike> {\n const open =\n fileSourceTestHooks.openFile ??\n (\n (await import('node:fs/promises')) as {\n open(path: FileSourcePath, flags: number): Promise<FileHandleLike>\n }\n ).open\n let file: FileHandleLike\n try {\n file = await open(path, await fileOpenFlags())\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n throw new Error(`FileSource: ${formatFilePath(path)} could not be opened: ${message}`)\n }\n\n try {\n const stats = await file.stat()\n assertRegularFile(path, stats)\n assertSameIdentity(path, identity, stats, 'before read')\n return file\n } catch (err) {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n throw err\n }\n}\n\nasync function lstatNodeFile(path: FileSourcePath): Promise<FileStatsLike> {\n const { lstat } = (await import('node:fs/promises')) as {\n lstat(path: FileSourcePath): Promise<FileStatsLike>\n }\n return lstat(path)\n}\n\nasync function readFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n signal?: AbortSignal,\n): Promise<Uint8Array> {\n throwIfAborted(signal)\n if (size === 0) {\n await verifyFileIdentityForEmptyRead(path, identity, signal)\n return new Uint8Array(0)\n }\n\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n const data = new Uint8Array(size)\n let filled = 0\n try {\n while (filled < data.byteLength) {\n throwIfAborted(signal)\n const length = Math.min(maxReadSize(), data.byteLength - filled)\n const { bytesRead } = await file.read(data, filled, length, offset + filled)\n if (bytesRead === 0) throw rangeEndedEarlyError(path, offset, size)\n filled += bytesRead\n await fileSourceTestHooks.afterReadIteration?.(filled)\n }\n throwIfAborted(signal)\n const stats = await file.stat()\n assertSameIdentity(path, identity, stats, 'while being read')\n return data\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nasync function verifyFileIdentityForEmptyRead(\n path: FileSourcePath,\n identity: FileIdentity,\n signal?: AbortSignal,\n): Promise<void> {\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n try {\n throwIfAborted(signal)\n return\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nfunction sliceFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n start: number,\n end: number,\n): ContentSource {\n const normalizedStart = normalizeSliceOffset(start, size)\n const normalizedEnd = normalizeSliceOffset(end, size)\n return new FileSliceSource(\n path,\n identity,\n offset + normalizedStart,\n Math.max(normalizedEnd - normalizedStart, 0),\n )\n}\n\nfunction streamFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n): ReadableStream<Uint8Array> {\n let position = offset\n let remaining = size\n let verifiedEmpty = false\n const abortController = new AbortController()\n\n // Open per pull so abandoned public streams do not strand file handles or\n // retain Windows locks. Each chunk still verifies identity around its read.\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (remaining === 0) {\n if (!verifiedEmpty) {\n verifiedEmpty = true\n await verifyFileIdentityForEmptyRead(path, identity)\n }\n controller.close()\n return\n }\n\n const length = Math.min(FILE_STREAM_CHUNK_SIZE, remaining)\n const data = await readFileRange(path, identity, position, length, abortController.signal)\n position += data.byteLength\n remaining -= data.byteLength\n controller.enqueue(data)\n if (remaining === 0) controller.close()\n } catch (err) {\n controller.error(err)\n }\n },\n cancel(reason) {\n abortController.abort(reason)\n },\n })\n}\n\nasync function fileRangeToArrayBuffer(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n options: ToArrayBufferOptions = {},\n): Promise<ArrayBuffer> {\n const data = await readFileRange(path, identity, offset, size, options.signal)\n return data.buffer as ArrayBuffer\n}\n\nclass FileSliceSource implements ContentSource {\n readonly canSlice = true\n\n constructor(\n private readonly path: FileSourcePath,\n private readonly identity: FileIdentity,\n private readonly offset: number,\n readonly size: number,\n ) {}\n\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, this.offset, this.size, start, end)\n }\n\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, this.offset, this.size)\n }\n\n toArrayBuffer(options: ToArrayBufferOptions = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, this.offset, this.size, options)\n }\n}\n\n/**\n * ContentSource backed by a local filesystem path.\n *\n * `FileSource` is Node-only but safe to import in browser builds: it touches\n * Node filesystem APIs only when constructed or read. The constructor performs\n * synchronous filesystem validation so `size` is immediately available; request\n * handlers, sync loops, and other latency-sensitive code should use\n * {@link FileSource.fromPath}. Both paths capture a best-effort regular file\n * identity and reject a symlink as the final path component; parent directory\n * symlinks are followed by the operating system, so callers that constrain\n * paths under a trusted root should validate those parents separately. Reads\n * reject if the path is replaced, if the filesystem cannot report stable\n * identity on POSIX platforms, or if size/mtime/ctime changes before the\n * configured byte range is read. On Windows, reads avoid unreliable dev/inode\n * identity comparisons and validate size/mtime instead.\n * Slices preserve the captured identity, so multipart uploads can read\n * disjoint ranges without materialising the whole file in memory or following\n * later leaf path swaps.\n */\nexport class FileSource implements ContentSource {\n /** Random-access: file ranges are read by absolute byte offset. */\n readonly canSlice = true\n /** File size captured at construction time. */\n readonly size: number\n private readonly path: FileSourcePath\n private readonly identity: FileIdentity\n\n /**\n * Create a FileSource for a local file path.\n * @param path - Local filesystem path or file URL.\n *\n * @throws If the runtime has no Node-compatible filesystem API.\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n constructor(path: FileSourcePath)\n /**\n * Internal constructor path for async validation.\n * @param path - Local filesystem path or file URL.\n * @param internal - Module-private validated identity payload.\n *\n * @internal\n */\n constructor(path: FileSourcePath, internal?: FileSourceInternalOptions) {\n const resolvedIdentity =\n internal?.key === FILE_SOURCE_INTERNAL\n ? internal.identity\n : validatedIdentityFromStats(path, getNodeFsSync().lstatSync(path))\n this.path = path\n this.identity = resolvedIdentity\n this.size = resolvedIdentity.size\n }\n\n /**\n * Return a new file-backed source covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, 0, this.size, start, end)\n }\n\n /**\n * Open this file as a Web ReadableStream.\n * @returns A ReadableStream that reads the file lazily.\n */\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, 0, this.size)\n }\n\n /**\n * Read this file into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise resolving with the file bytes.\n */\n toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, 0, this.size, options)\n }\n\n /**\n * Create a FileSource using asynchronous filesystem validation.\n * @param path - Local filesystem path or file URL.\n *\n * @returns A FileSource bound to the validated file identity.\n *\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n static async fromPath(path: FileSourcePath): Promise<FileSource> {\n const identity = validatedIdentityFromStats(path, await lstatNodeFile(path))\n return constructFileSourceFromIdentity(path, identity)\n }\n}\n\nfunction constructFileSourceFromIdentity(path: FileSourcePath, identity: FileIdentity): FileSource {\n // The public overload is intentionally single-argument; Reflect.construct\n // lets fromPath pass the module-private validated identity without exposing\n // that constructor shape in the public type surface.\n return Reflect.construct(FileSource, [\n path,\n { key: FILE_SOURCE_INTERNAL, identity },\n ]) as FileSource\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-source.cjs","names":[],"sources":["../../src/streams/file-source.ts"],"sourcesContent":["import type { ContentSource } from './source.ts'\n\nconst FILE_STREAM_CHUNK_SIZE = 16 * 1024 * 1024\n\n/** Filesystem path accepted by {@link FileSource}. */\nexport type FileSourcePath = string | URL\n\ninterface FileStatsLike {\n readonly dev: number\n readonly ino: number\n readonly mode: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n isFile(): boolean\n}\n\ninterface FileHandleLike {\n read(\n buffer: Uint8Array,\n offset: number,\n length: number,\n position: number,\n ): Promise<{ bytesRead: number }>\n stat(): Promise<FileStatsLike>\n close(): Promise<void>\n}\n\ninterface NodeFsSync {\n lstatSync(path: FileSourcePath): FileStatsLike\n}\n\ninterface FileIdentity {\n readonly dev: number\n readonly ino: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n}\n\ninterface ToArrayBufferOptions {\n readonly signal?: AbortSignal\n}\n\nconst FILE_SOURCE_INTERNAL = Symbol('FileSource.internal')\n\ninterface FileSourceInternalOptions {\n readonly key: typeof FILE_SOURCE_INTERNAL\n readonly identity: FileIdentity\n}\n\n/** @internal */\nexport const fileSourceTestHooks: {\n afterReadIteration?: (filled: number) => Promise<void> | void\n maxReadSize?: number\n openFile?: (path: FileSourcePath, flags: number) => Promise<FileHandleLike>\n platform?: string\n} = {}\n\nfunction getNodeFsSync(): NodeFsSync {\n // FileSource exposes `size` synchronously, so the constructor cannot use the\n // repo's usual `await import('node:...')` pattern for Node-only APIs. Keep the\n // synchronous lookup here; async callers should prefer `FileSource.fromPath()`.\n const processLike = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown }\n }\n ).process\n const fs = processLike?.getBuiltinModule?.('node:fs')\n if (!isNodeFsSync(fs)) {\n throw new Error(\n 'FileSource constructor requires Node.js 22.3+ synchronous filesystem APIs; use FileSource.fromPath() when synchronous filesystem access is unavailable.',\n )\n }\n return fs\n}\n\nfunction isNodeFsSync(value: unknown): value is NodeFsSync {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Record<string, unknown>\n return typeof candidate['lstatSync'] === 'function'\n}\n\nasync function fileOpenFlags(): Promise<number> {\n const { constants } = await import('node:fs')\n // O_NOFOLLOW and O_NONBLOCK are unavailable on some platforms. When\n // O_NOFOLLOW is absent, the post-open fstat identity check below is the\n // symlink-swap defense. O_NONBLOCK avoids blocking indefinitely if a path is\n // swapped to a FIFO or another blocking special file before fstat rejects it.\n return (\n constants.O_RDONLY |\n (constants.O_NOFOLLOW ?? 0) |\n ((constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0)\n )\n}\n\nfunction normalizeSliceOffset(value: number, size: number): number {\n if (!Number.isFinite(value)) throw new RangeError('FileSource slice offsets must be finite.')\n const integer = Math.trunc(value)\n const offset = integer < 0 ? size + integer : integer\n return Math.min(Math.max(offset, 0), size)\n}\n\nfunction formatFilePath(path: FileSourcePath): string {\n return path instanceof URL ? path.href : path\n}\n\nfunction identityFromStats(stats: FileStatsLike): FileIdentity {\n return {\n dev: stats.dev,\n ino: stats.ino,\n size: stats.size,\n mtimeMs: stats.mtimeMs,\n ctimeMs: stats.ctimeMs,\n }\n}\n\nfunction assertStableIdentity(path: FileSourcePath, stats: FileStatsLike): void {\n if (stats.dev === 0 && stats.ino === 0) {\n throw new Error(\n `FileSource: ${formatFilePath(path)} is on a filesystem that does not expose stable file identity.`,\n )\n }\n}\n\nfunction assertRegularFile(path: FileSourcePath, stats: FileStatsLike): void {\n if (!stats.isFile()) {\n throw new Error(`FileSource: ${formatFilePath(path)} is not a regular file.`)\n }\n}\n\nfunction validatedIdentityFromStats(path: FileSourcePath, stats: FileStatsLike): FileIdentity {\n assertRegularFile(path, stats)\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, stats)\n return identityFromStats(stats)\n}\n\nfunction assertSameIdentity(\n path: FileSourcePath,\n expected: FileIdentity,\n actual: FileStatsLike,\n when: string,\n): void {\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, actual)\n if (\n shouldComparePosixFileIdentity() &&\n (actual.dev !== expected.dev || actual.ino !== expected.ino)\n ) {\n throw new Error(`FileSource: ${formatFilePath(path)} changed ${when}.`)\n }\n if (actual.size !== expected.size || actual.mtimeMs !== expected.mtimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n if (shouldComparePosixChangeTime() && actual.ctimeMs !== expected.ctimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n}\n\nfunction isWindows(): boolean {\n if (fileSourceTestHooks.platform !== undefined) return fileSourceTestHooks.platform === 'win32'\n const processLike = (globalThis as { process?: { platform?: string } }).process\n return processLike?.platform === 'win32'\n}\n\nfunction shouldComparePosixFileIdentity(): boolean {\n return !isWindows()\n}\n\nfunction shouldComparePosixChangeTime(): boolean {\n return !isWindows()\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal === undefined || !signal.aborted) return\n throw signal.reason ?? new DOMException('Aborted', 'AbortError')\n}\n\nfunction maxReadSize(): number {\n return fileSourceTestHooks.maxReadSize ?? FILE_STREAM_CHUNK_SIZE\n}\n\n/* v8 ignore start -- Requires a file to pass identity checks and still EOF mid-range. */\nfunction rangeEndedEarlyError(path: FileSourcePath, offset: number, size: number): Error {\n const end = offset + size\n return new Error(\n `FileSource: ${formatFilePath(path)} ended before byte range [${offset}, ${end}) was fully read.`,\n )\n}\n/* v8 ignore stop */\n\nasync function openValidatedFile(\n path: FileSourcePath,\n identity: FileIdentity,\n): Promise<FileHandleLike> {\n const open =\n fileSourceTestHooks.openFile ??\n (\n (await import('node:fs/promises')) as {\n open(path: FileSourcePath, flags: number): Promise<FileHandleLike>\n }\n ).open\n let file: FileHandleLike\n try {\n file = await open(path, await fileOpenFlags())\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n throw new Error(`FileSource: ${formatFilePath(path)} could not be opened: ${message}`)\n }\n\n try {\n const stats = await file.stat()\n assertRegularFile(path, stats)\n assertSameIdentity(path, identity, stats, 'before read')\n return file\n } catch (err) {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n throw err\n }\n}\n\nasync function lstatNodeFile(path: FileSourcePath): Promise<FileStatsLike> {\n const { lstat } = (await import('node:fs/promises')) as {\n lstat(path: FileSourcePath): Promise<FileStatsLike>\n }\n return lstat(path)\n}\n\nasync function readFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n signal?: AbortSignal,\n): Promise<Uint8Array> {\n throwIfAborted(signal)\n if (size === 0) {\n await verifyFileIdentityForEmptyRead(path, identity, signal)\n return new Uint8Array(0)\n }\n\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n const data = new Uint8Array(size)\n let filled = 0\n try {\n while (filled < data.byteLength) {\n throwIfAborted(signal)\n const length = Math.min(maxReadSize(), data.byteLength - filled)\n const { bytesRead } = await file.read(data, filled, length, offset + filled)\n if (bytesRead === 0) throw rangeEndedEarlyError(path, offset, size)\n filled += bytesRead\n await fileSourceTestHooks.afterReadIteration?.(filled)\n }\n throwIfAborted(signal)\n const stats = await file.stat()\n assertSameIdentity(path, identity, stats, 'while being read')\n return data\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nasync function verifyFileIdentityForEmptyRead(\n path: FileSourcePath,\n identity: FileIdentity,\n signal?: AbortSignal,\n): Promise<void> {\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n try {\n throwIfAborted(signal)\n return\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nfunction sliceFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n start: number,\n end: number,\n): ContentSource {\n const normalizedStart = normalizeSliceOffset(start, size)\n const normalizedEnd = normalizeSliceOffset(end, size)\n return new FileSliceSource(\n path,\n identity,\n offset + normalizedStart,\n Math.max(normalizedEnd - normalizedStart, 0),\n )\n}\n\nfunction streamFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n): ReadableStream<Uint8Array> {\n let position = offset\n let remaining = size\n let verifiedEmpty = false\n const abortController = new AbortController()\n\n // Open per pull so abandoned public streams do not strand file handles or\n // retain Windows locks. Each chunk still verifies identity around its read.\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (remaining === 0) {\n if (!verifiedEmpty) {\n verifiedEmpty = true\n await verifyFileIdentityForEmptyRead(path, identity)\n }\n controller.close()\n return\n }\n\n const length = Math.min(FILE_STREAM_CHUNK_SIZE, remaining)\n const data = await readFileRange(path, identity, position, length, abortController.signal)\n position += data.byteLength\n remaining -= data.byteLength\n controller.enqueue(data)\n if (remaining === 0) controller.close()\n } catch (err) {\n controller.error(err)\n }\n },\n cancel(reason) {\n abortController.abort(reason)\n },\n })\n}\n\nasync function fileRangeToArrayBuffer(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n options: ToArrayBufferOptions = {},\n): Promise<ArrayBuffer> {\n const data = await readFileRange(path, identity, offset, size, options.signal)\n return data.buffer as ArrayBuffer\n}\n\nclass FileSliceSource implements ContentSource {\n readonly canSlice = true\n\n constructor(\n private readonly path: FileSourcePath,\n private readonly identity: FileIdentity,\n private readonly offset: number,\n readonly size: number,\n ) {}\n\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, this.offset, this.size, start, end)\n }\n\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, this.offset, this.size)\n }\n\n toArrayBuffer(options: ToArrayBufferOptions = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, this.offset, this.size, options)\n }\n}\n\n/**\n * ContentSource backed by a local filesystem path.\n *\n * `FileSource` is Node-only but safe to import in browser builds: it touches\n * Node filesystem APIs only when constructed or read. The constructor performs\n * synchronous filesystem validation so `size` is immediately available; request\n * handlers, sync loops, and other latency-sensitive code should use\n * {@link FileSource.fromPath}. Both paths capture a best-effort regular file\n * identity and reject a symlink as the final path component; parent directory\n * symlinks are followed by the operating system, so callers that constrain\n * paths under a trusted root should validate those parents separately. Reads\n * reject if the path is replaced, if the filesystem cannot report stable\n * identity on POSIX platforms, or if size/mtime/ctime changes before the\n * configured byte range is read. On Windows, reads avoid unreliable dev/inode\n * identity comparisons and validate size/mtime instead.\n * Slices preserve the captured identity, so multipart uploads can read\n * disjoint ranges without materialising the whole file in memory or following\n * later leaf path swaps.\n */\nexport class FileSource implements ContentSource {\n /** Random-access: file ranges are read by absolute byte offset. */\n readonly canSlice = true\n /** File size captured at construction time. */\n readonly size: number\n private readonly path: FileSourcePath\n private readonly identity: FileIdentity\n\n /**\n * Create a FileSource for a local file path.\n * @param path - Local filesystem path or file URL.\n *\n * @throws If the runtime has no Node-compatible filesystem API.\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n constructor(path: FileSourcePath)\n /**\n * Internal constructor path for async validation.\n * @param path - Local filesystem path or file URL.\n * @param internal - Module-private validated identity payload.\n *\n * @internal\n */\n constructor(path: FileSourcePath, internal?: FileSourceInternalOptions) {\n const resolvedIdentity =\n internal?.key === FILE_SOURCE_INTERNAL\n ? internal.identity\n : validatedIdentityFromStats(path, getNodeFsSync().lstatSync(path))\n this.path = path\n this.identity = resolvedIdentity\n this.size = resolvedIdentity.size\n }\n\n /**\n * Return a new file-backed source covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, 0, this.size, start, end)\n }\n\n /**\n * Open this file as a Web ReadableStream.\n * @returns A ReadableStream that reads the file lazily.\n */\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, 0, this.size)\n }\n\n /**\n * Read this file into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise resolving with the file bytes.\n */\n toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, 0, this.size, options)\n }\n\n /**\n * Create a FileSource using asynchronous filesystem validation.\n * @param path - Local filesystem path or file URL.\n *\n * @returns A FileSource bound to the validated file identity.\n *\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n static async fromPath(path: FileSourcePath): Promise<FileSource> {\n const identity = validatedIdentityFromStats(path, await lstatNodeFile(path))\n return constructFileSourceFromIdentity(path, identity)\n }\n}\n\nfunction constructFileSourceFromIdentity(path: FileSourcePath, identity: FileIdentity): FileSource {\n // The public overload is intentionally single-argument; Reflect.construct\n // lets fromPath pass the module-private validated identity without exposing\n // that constructor shape in the public type surface.\n return Reflect.construct(FileSource, [\n path,\n { key: FILE_SOURCE_INTERNAL, identity },\n ]) as FileSource\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-source.js","names":[],"sources":["../../src/streams/file-source.ts"],"sourcesContent":["import type { ContentSource } from './source.ts'\n\nconst FILE_STREAM_CHUNK_SIZE = 16 * 1024 * 1024\n\n/** Filesystem path accepted by {@link FileSource}. */\nexport type FileSourcePath = string | URL\n\ninterface FileStatsLike {\n readonly dev: number\n readonly ino: number\n readonly mode: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n isFile(): boolean\n}\n\ninterface FileHandleLike {\n read(\n buffer: Uint8Array,\n offset: number,\n length: number,\n position: number,\n ): Promise<{ bytesRead: number }>\n stat(): Promise<FileStatsLike>\n close(): Promise<void>\n}\n\ninterface NodeFsSync {\n lstatSync(path: FileSourcePath): FileStatsLike\n}\n\ninterface FileIdentity {\n readonly dev: number\n readonly ino: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n}\n\ninterface ToArrayBufferOptions {\n readonly signal?: AbortSignal\n}\n\nconst FILE_SOURCE_INTERNAL = Symbol('FileSource.internal')\n\ninterface FileSourceInternalOptions {\n readonly key: typeof FILE_SOURCE_INTERNAL\n readonly identity: FileIdentity\n}\n\n/** @internal */\nexport const fileSourceTestHooks: {\n afterReadIteration?: (filled: number) => Promise<void> | void\n maxReadSize?: number\n openFile?: (path: FileSourcePath, flags: number) => Promise<FileHandleLike>\n platform?: string\n} = {}\n\nfunction getNodeFsSync(): NodeFsSync {\n // FileSource exposes `size` synchronously, so the constructor cannot use the\n // repo's usual `await import('node:...')` pattern for Node-only APIs. Keep the\n // synchronous lookup here; async callers should prefer `FileSource.fromPath()`.\n const processLike = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown }\n }\n ).process\n const fs = processLike?.getBuiltinModule?.('node:fs')\n if (!isNodeFsSync(fs)) {\n throw new Error(\n 'FileSource constructor requires Node.js 22.3+ synchronous filesystem APIs; use FileSource.fromPath() when synchronous filesystem access is unavailable.',\n )\n }\n return fs\n}\n\nfunction isNodeFsSync(value: unknown): value is NodeFsSync {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Record<string, unknown>\n return typeof candidate['lstatSync'] === 'function'\n}\n\nasync function fileOpenFlags(): Promise<number> {\n const { constants } = await import('node:fs')\n // O_NOFOLLOW and O_NONBLOCK are unavailable on some platforms. When\n // O_NOFOLLOW is absent, the post-open fstat identity check below is the\n // symlink-swap defense. O_NONBLOCK avoids blocking indefinitely if a path is\n // swapped to a FIFO or another blocking special file before fstat rejects it.\n return (\n constants.O_RDONLY |\n (constants.O_NOFOLLOW ?? 0) |\n ((constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0)\n )\n}\n\nfunction normalizeSliceOffset(value: number, size: number): number {\n if (!Number.isFinite(value)) throw new RangeError('FileSource slice offsets must be finite.')\n const integer = Math.trunc(value)\n const offset = integer < 0 ? size + integer : integer\n return Math.min(Math.max(offset, 0), size)\n}\n\nfunction formatFilePath(path: FileSourcePath): string {\n return path instanceof URL ? path.href : path\n}\n\nfunction identityFromStats(stats: FileStatsLike): FileIdentity {\n return {\n dev: stats.dev,\n ino: stats.ino,\n size: stats.size,\n mtimeMs: stats.mtimeMs,\n ctimeMs: stats.ctimeMs,\n }\n}\n\nfunction assertStableIdentity(path: FileSourcePath, stats: FileStatsLike): void {\n if (stats.dev === 0 && stats.ino === 0) {\n throw new Error(\n `FileSource: ${formatFilePath(path)} is on a filesystem that does not expose stable file identity.`,\n )\n }\n}\n\nfunction assertRegularFile(path: FileSourcePath, stats: FileStatsLike): void {\n if (!stats.isFile()) {\n throw new Error(`FileSource: ${formatFilePath(path)} is not a regular file.`)\n }\n}\n\nfunction validatedIdentityFromStats(path: FileSourcePath, stats: FileStatsLike): FileIdentity {\n assertRegularFile(path, stats)\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, stats)\n return identityFromStats(stats)\n}\n\nfunction assertSameIdentity(\n path: FileSourcePath,\n expected: FileIdentity,\n actual: FileStatsLike,\n when: string,\n): void {\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, actual)\n if (\n shouldComparePosixFileIdentity() &&\n (actual.dev !== expected.dev || actual.ino !== expected.ino)\n ) {\n throw new Error(`FileSource: ${formatFilePath(path)} changed ${when}.`)\n }\n if (actual.size !== expected.size || actual.mtimeMs !== expected.mtimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n if (shouldComparePosixChangeTime() && actual.ctimeMs !== expected.ctimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n}\n\nfunction isWindows(): boolean {\n if (fileSourceTestHooks.platform !== undefined) return fileSourceTestHooks.platform === 'win32'\n const processLike = (globalThis as { process?: { platform?: string } }).process\n return processLike?.platform === 'win32'\n}\n\nfunction shouldComparePosixFileIdentity(): boolean {\n return !isWindows()\n}\n\nfunction shouldComparePosixChangeTime(): boolean {\n return !isWindows()\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal === undefined || !signal.aborted) return\n throw signal.reason ?? new DOMException('Aborted', 'AbortError')\n}\n\nfunction maxReadSize(): number {\n return fileSourceTestHooks.maxReadSize ?? FILE_STREAM_CHUNK_SIZE\n}\n\n/* v8 ignore start -- Requires a file to pass identity checks and still EOF mid-range. */\nfunction rangeEndedEarlyError(path: FileSourcePath, offset: number, size: number): Error {\n const end = offset + size\n return new Error(\n `FileSource: ${formatFilePath(path)} ended before byte range [${offset}, ${end}) was fully read.`,\n )\n}\n/* v8 ignore stop */\n\nasync function openValidatedFile(\n path: FileSourcePath,\n identity: FileIdentity,\n): Promise<FileHandleLike> {\n const open =\n fileSourceTestHooks.openFile ??\n (\n (await import('node:fs/promises')) as {\n open(path: FileSourcePath, flags: number): Promise<FileHandleLike>\n }\n ).open\n let file: FileHandleLike\n try {\n file = await open(path, await fileOpenFlags())\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n throw new Error(`FileSource: ${formatFilePath(path)} could not be opened: ${message}`)\n }\n\n try {\n const stats = await file.stat()\n assertRegularFile(path, stats)\n assertSameIdentity(path, identity, stats, 'before read')\n return file\n } catch (err) {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n throw err\n }\n}\n\nasync function lstatNodeFile(path: FileSourcePath): Promise<FileStatsLike> {\n const { lstat } = (await import('node:fs/promises')) as {\n lstat(path: FileSourcePath): Promise<FileStatsLike>\n }\n return lstat(path)\n}\n\nasync function readFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n signal?: AbortSignal,\n): Promise<Uint8Array> {\n throwIfAborted(signal)\n if (size === 0) {\n await verifyFileIdentityForEmptyRead(path, identity, signal)\n return new Uint8Array(0)\n }\n\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n const data = new Uint8Array(size)\n let filled = 0\n try {\n while (filled < data.byteLength) {\n throwIfAborted(signal)\n const length = Math.min(maxReadSize(), data.byteLength - filled)\n const { bytesRead } = await file.read(data, filled, length, offset + filled)\n if (bytesRead === 0) throw rangeEndedEarlyError(path, offset, size)\n filled += bytesRead\n await fileSourceTestHooks.afterReadIteration?.(filled)\n }\n throwIfAborted(signal)\n const stats = await file.stat()\n assertSameIdentity(path, identity, stats, 'while being read')\n return data\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nasync function verifyFileIdentityForEmptyRead(\n path: FileSourcePath,\n identity: FileIdentity,\n signal?: AbortSignal,\n): Promise<void> {\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n try {\n throwIfAborted(signal)\n return\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nfunction sliceFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n start: number,\n end: number,\n): ContentSource {\n const normalizedStart = normalizeSliceOffset(start, size)\n const normalizedEnd = normalizeSliceOffset(end, size)\n return new FileSliceSource(\n path,\n identity,\n offset + normalizedStart,\n Math.max(normalizedEnd - normalizedStart, 0),\n )\n}\n\nfunction streamFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n): ReadableStream<Uint8Array> {\n let position = offset\n let remaining = size\n let verifiedEmpty = false\n const abortController = new AbortController()\n\n // Open per pull so abandoned public streams do not strand file handles or\n // retain Windows locks. Each chunk still verifies identity around its read.\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (remaining === 0) {\n if (!verifiedEmpty) {\n verifiedEmpty = true\n await verifyFileIdentityForEmptyRead(path, identity)\n }\n controller.close()\n return\n }\n\n const length = Math.min(FILE_STREAM_CHUNK_SIZE, remaining)\n const data = await readFileRange(path, identity, position, length, abortController.signal)\n position += data.byteLength\n remaining -= data.byteLength\n controller.enqueue(data)\n if (remaining === 0) controller.close()\n } catch (err) {\n controller.error(err)\n }\n },\n cancel(reason) {\n abortController.abort(reason)\n },\n })\n}\n\nasync function fileRangeToArrayBuffer(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n options: ToArrayBufferOptions = {},\n): Promise<ArrayBuffer> {\n const data = await readFileRange(path, identity, offset, size, options.signal)\n return data.buffer as ArrayBuffer\n}\n\nclass FileSliceSource implements ContentSource {\n readonly canSlice = true\n\n constructor(\n private readonly path: FileSourcePath,\n private readonly identity: FileIdentity,\n private readonly offset: number,\n readonly size: number,\n ) {}\n\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, this.offset, this.size, start, end)\n }\n\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, this.offset, this.size)\n }\n\n toArrayBuffer(options: ToArrayBufferOptions = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, this.offset, this.size, options)\n }\n}\n\n/**\n * ContentSource backed by a local filesystem path.\n *\n * `FileSource` is Node-only but safe to import in browser builds: it touches\n * Node filesystem APIs only when constructed or read. The constructor performs\n * synchronous filesystem validation so `size` is immediately available; request\n * handlers, sync loops, and other latency-sensitive code should use\n * {@link FileSource.fromPath}. Both paths capture a best-effort regular file\n * identity and reject a symlink as the final path component; parent directory\n * symlinks are followed by the operating system, so callers that constrain\n * paths under a trusted root should validate those parents separately. Reads\n * reject if the path is replaced, if the filesystem cannot report stable\n * identity on POSIX platforms, or if size/mtime/ctime changes before the\n * configured byte range is read. On Windows, reads avoid unreliable dev/inode\n * identity comparisons and validate size/mtime instead.\n * Slices preserve the captured identity, so multipart uploads can read\n * disjoint ranges without materialising the whole file in memory or following\n * later leaf path swaps.\n */\nexport class FileSource implements ContentSource {\n /** Random-access: file ranges are read by absolute byte offset. */\n readonly canSlice = true\n /** File size captured at construction time. */\n readonly size: number\n private readonly path: FileSourcePath\n private readonly identity: FileIdentity\n\n /**\n * Create a FileSource for a local file path.\n * @param path - Local filesystem path or file URL.\n *\n * @throws If the runtime has no Node-compatible filesystem API.\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n constructor(path: FileSourcePath)\n /**\n * Internal constructor path for async validation.\n * @param path - Local filesystem path or file URL.\n * @param internal - Module-private validated identity payload.\n *\n * @internal\n */\n constructor(path: FileSourcePath, internal?: FileSourceInternalOptions) {\n const resolvedIdentity =\n internal?.key === FILE_SOURCE_INTERNAL\n ? internal.identity\n : validatedIdentityFromStats(path, getNodeFsSync().lstatSync(path))\n this.path = path\n this.identity = resolvedIdentity\n this.size = resolvedIdentity.size\n }\n\n /**\n * Return a new file-backed source covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, 0, this.size, start, end)\n }\n\n /**\n * Open this file as a Web ReadableStream.\n * @returns A ReadableStream that reads the file lazily.\n */\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, 0, this.size)\n }\n\n /**\n * Read this file into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise resolving with the file bytes.\n */\n toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, 0, this.size, options)\n }\n\n /**\n * Create a FileSource using asynchronous filesystem validation.\n * @param path - Local filesystem path or file URL.\n *\n * @returns A FileSource bound to the validated file identity.\n *\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n static async fromPath(path: FileSourcePath): Promise<FileSource> {\n const identity = validatedIdentityFromStats(path, await lstatNodeFile(path))\n return constructFileSourceFromIdentity(path, identity)\n }\n}\n\nfunction constructFileSourceFromIdentity(path: FileSourcePath, identity: FileIdentity): FileSource {\n // The public overload is intentionally single-argument; Reflect.construct\n // lets fromPath pass the module-private validated identity without exposing\n // that constructor shape in the public type surface.\n return Reflect.construct(FileSource, [\n path,\n { key: FILE_SOURCE_INTERNAL, identity },\n ]) as FileSource\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-source.js","names":[],"sources":["../../src/streams/file-source.ts"],"sourcesContent":["import type { ContentSource } from './source.ts'\n\nconst FILE_STREAM_CHUNK_SIZE = 16 * 1024 * 1024\n\n/** Filesystem path accepted by {@link FileSource}. */\nexport type FileSourcePath = string | URL\n\ninterface FileStatsLike {\n readonly dev: number\n readonly ino: number\n readonly mode: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n isFile(): boolean\n}\n\ninterface FileHandleLike {\n read(\n buffer: Uint8Array,\n offset: number,\n length: number,\n position: number,\n ): Promise<{ bytesRead: number }>\n stat(): Promise<FileStatsLike>\n close(): Promise<void>\n}\n\ninterface NodeFsSync {\n lstatSync(path: FileSourcePath): FileStatsLike\n}\n\ninterface FileIdentity {\n readonly dev: number\n readonly ino: number\n readonly size: number\n readonly mtimeMs: number\n readonly ctimeMs: number\n}\n\ninterface ToArrayBufferOptions {\n readonly signal?: AbortSignal\n}\n\nconst FILE_SOURCE_INTERNAL = Symbol('FileSource.internal')\n\ninterface FileSourceInternalOptions {\n readonly key: typeof FILE_SOURCE_INTERNAL\n readonly identity: FileIdentity\n}\n\n/** @internal */\nexport const fileSourceTestHooks: {\n afterReadIteration?: (filled: number) => Promise<void> | void\n maxReadSize?: number\n openFile?: (path: FileSourcePath, flags: number) => Promise<FileHandleLike>\n platform?: string\n} = {}\n\nfunction getNodeFsSync(): NodeFsSync {\n // FileSource exposes `size` synchronously, so the constructor cannot use the\n // repo's usual `await import('node:...')` pattern for Node-only APIs. Keep the\n // synchronous lookup here; async callers should prefer `FileSource.fromPath()`.\n const processLike = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown }\n }\n ).process\n const fs = processLike?.getBuiltinModule?.('node:fs')\n if (!isNodeFsSync(fs)) {\n throw new Error(\n 'FileSource constructor requires Node.js 22.3+ synchronous filesystem APIs; use FileSource.fromPath() when synchronous filesystem access is unavailable.',\n )\n }\n return fs\n}\n\nfunction isNodeFsSync(value: unknown): value is NodeFsSync {\n if (typeof value !== 'object' || value === null) return false\n const candidate = value as Record<string, unknown>\n return typeof candidate['lstatSync'] === 'function'\n}\n\nasync function fileOpenFlags(): Promise<number> {\n const { constants } = await import('node:fs')\n // O_NOFOLLOW and O_NONBLOCK are unavailable on some platforms. When\n // O_NOFOLLOW is absent, the post-open fstat identity check below is the\n // symlink-swap defense. O_NONBLOCK avoids blocking indefinitely if a path is\n // swapped to a FIFO or another blocking special file before fstat rejects it.\n return (\n constants.O_RDONLY |\n (constants.O_NOFOLLOW ?? 0) |\n ((constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0)\n )\n}\n\nfunction normalizeSliceOffset(value: number, size: number): number {\n if (!Number.isFinite(value)) throw new RangeError('FileSource slice offsets must be finite.')\n const integer = Math.trunc(value)\n const offset = integer < 0 ? size + integer : integer\n return Math.min(Math.max(offset, 0), size)\n}\n\nfunction formatFilePath(path: FileSourcePath): string {\n return path instanceof URL ? path.href : path\n}\n\nfunction identityFromStats(stats: FileStatsLike): FileIdentity {\n return {\n dev: stats.dev,\n ino: stats.ino,\n size: stats.size,\n mtimeMs: stats.mtimeMs,\n ctimeMs: stats.ctimeMs,\n }\n}\n\nfunction assertStableIdentity(path: FileSourcePath, stats: FileStatsLike): void {\n if (stats.dev === 0 && stats.ino === 0) {\n throw new Error(\n `FileSource: ${formatFilePath(path)} is on a filesystem that does not expose stable file identity.`,\n )\n }\n}\n\nfunction assertRegularFile(path: FileSourcePath, stats: FileStatsLike): void {\n if (!stats.isFile()) {\n throw new Error(`FileSource: ${formatFilePath(path)} is not a regular file.`)\n }\n}\n\nfunction validatedIdentityFromStats(path: FileSourcePath, stats: FileStatsLike): FileIdentity {\n assertRegularFile(path, stats)\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, stats)\n return identityFromStats(stats)\n}\n\nfunction assertSameIdentity(\n path: FileSourcePath,\n expected: FileIdentity,\n actual: FileStatsLike,\n when: string,\n): void {\n if (shouldComparePosixFileIdentity()) assertStableIdentity(path, actual)\n if (\n shouldComparePosixFileIdentity() &&\n (actual.dev !== expected.dev || actual.ino !== expected.ino)\n ) {\n throw new Error(`FileSource: ${formatFilePath(path)} changed ${when}.`)\n }\n if (actual.size !== expected.size || actual.mtimeMs !== expected.mtimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n if (shouldComparePosixChangeTime() && actual.ctimeMs !== expected.ctimeMs) {\n throw new Error(`FileSource: ${formatFilePath(path)} was modified ${when}.`)\n }\n}\n\nfunction isWindows(): boolean {\n if (fileSourceTestHooks.platform !== undefined) return fileSourceTestHooks.platform === 'win32'\n const processLike = (globalThis as { process?: { platform?: string } }).process\n return processLike?.platform === 'win32'\n}\n\nfunction shouldComparePosixFileIdentity(): boolean {\n return !isWindows()\n}\n\nfunction shouldComparePosixChangeTime(): boolean {\n return !isWindows()\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n if (signal === undefined || !signal.aborted) return\n throw signal.reason ?? new DOMException('Aborted', 'AbortError')\n}\n\nfunction maxReadSize(): number {\n return fileSourceTestHooks.maxReadSize ?? FILE_STREAM_CHUNK_SIZE\n}\n\n/* v8 ignore start -- Requires a file to pass identity checks and still EOF mid-range. */\nfunction rangeEndedEarlyError(path: FileSourcePath, offset: number, size: number): Error {\n const end = offset + size\n return new Error(\n `FileSource: ${formatFilePath(path)} ended before byte range [${offset}, ${end}) was fully read.`,\n )\n}\n/* v8 ignore stop */\n\nasync function openValidatedFile(\n path: FileSourcePath,\n identity: FileIdentity,\n): Promise<FileHandleLike> {\n const open =\n fileSourceTestHooks.openFile ??\n (\n (await import('node:fs/promises')) as {\n open(path: FileSourcePath, flags: number): Promise<FileHandleLike>\n }\n ).open\n let file: FileHandleLike\n try {\n file = await open(path, await fileOpenFlags())\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n throw new Error(`FileSource: ${formatFilePath(path)} could not be opened: ${message}`)\n }\n\n try {\n const stats = await file.stat()\n assertRegularFile(path, stats)\n assertSameIdentity(path, identity, stats, 'before read')\n return file\n } catch (err) {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n throw err\n }\n}\n\nasync function lstatNodeFile(path: FileSourcePath): Promise<FileStatsLike> {\n const { lstat } = (await import('node:fs/promises')) as {\n lstat(path: FileSourcePath): Promise<FileStatsLike>\n }\n return lstat(path)\n}\n\nasync function readFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n signal?: AbortSignal,\n): Promise<Uint8Array> {\n throwIfAborted(signal)\n if (size === 0) {\n await verifyFileIdentityForEmptyRead(path, identity, signal)\n return new Uint8Array(0)\n }\n\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n const data = new Uint8Array(size)\n let filled = 0\n try {\n while (filled < data.byteLength) {\n throwIfAborted(signal)\n const length = Math.min(maxReadSize(), data.byteLength - filled)\n const { bytesRead } = await file.read(data, filled, length, offset + filled)\n if (bytesRead === 0) throw rangeEndedEarlyError(path, offset, size)\n filled += bytesRead\n await fileSourceTestHooks.afterReadIteration?.(filled)\n }\n throwIfAborted(signal)\n const stats = await file.stat()\n assertSameIdentity(path, identity, stats, 'while being read')\n return data\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nasync function verifyFileIdentityForEmptyRead(\n path: FileSourcePath,\n identity: FileIdentity,\n signal?: AbortSignal,\n): Promise<void> {\n throwIfAborted(signal)\n const file = await openValidatedFile(path, identity)\n try {\n throwIfAborted(signal)\n return\n } finally {\n /* v8 ignore next -- Cleanup failure is deliberately best-effort. */\n await file.close().catch(() => {})\n }\n}\n\nfunction sliceFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n start: number,\n end: number,\n): ContentSource {\n const normalizedStart = normalizeSliceOffset(start, size)\n const normalizedEnd = normalizeSliceOffset(end, size)\n return new FileSliceSource(\n path,\n identity,\n offset + normalizedStart,\n Math.max(normalizedEnd - normalizedStart, 0),\n )\n}\n\nfunction streamFileRange(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n): ReadableStream<Uint8Array> {\n let position = offset\n let remaining = size\n let verifiedEmpty = false\n const abortController = new AbortController()\n\n // Open per pull so abandoned public streams do not strand file handles or\n // retain Windows locks. Each chunk still verifies identity around its read.\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (remaining === 0) {\n if (!verifiedEmpty) {\n verifiedEmpty = true\n await verifyFileIdentityForEmptyRead(path, identity)\n }\n controller.close()\n return\n }\n\n const length = Math.min(FILE_STREAM_CHUNK_SIZE, remaining)\n const data = await readFileRange(path, identity, position, length, abortController.signal)\n position += data.byteLength\n remaining -= data.byteLength\n controller.enqueue(data)\n if (remaining === 0) controller.close()\n } catch (err) {\n controller.error(err)\n }\n },\n cancel(reason) {\n abortController.abort(reason)\n },\n })\n}\n\nasync function fileRangeToArrayBuffer(\n path: FileSourcePath,\n identity: FileIdentity,\n offset: number,\n size: number,\n options: ToArrayBufferOptions = {},\n): Promise<ArrayBuffer> {\n const data = await readFileRange(path, identity, offset, size, options.signal)\n return data.buffer as ArrayBuffer\n}\n\nclass FileSliceSource implements ContentSource {\n readonly canSlice = true\n\n constructor(\n private readonly path: FileSourcePath,\n private readonly identity: FileIdentity,\n private readonly offset: number,\n readonly size: number,\n ) {}\n\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, this.offset, this.size, start, end)\n }\n\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, this.offset, this.size)\n }\n\n toArrayBuffer(options: ToArrayBufferOptions = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, this.offset, this.size, options)\n }\n}\n\n/**\n * ContentSource backed by a local filesystem path.\n *\n * `FileSource` is Node-only but safe to import in browser builds: it touches\n * Node filesystem APIs only when constructed or read. The constructor performs\n * synchronous filesystem validation so `size` is immediately available; request\n * handlers, sync loops, and other latency-sensitive code should use\n * {@link FileSource.fromPath}. Both paths capture a best-effort regular file\n * identity and reject a symlink as the final path component; parent directory\n * symlinks are followed by the operating system, so callers that constrain\n * paths under a trusted root should validate those parents separately. Reads\n * reject if the path is replaced, if the filesystem cannot report stable\n * identity on POSIX platforms, or if size/mtime/ctime changes before the\n * configured byte range is read. On Windows, reads avoid unreliable dev/inode\n * identity comparisons and validate size/mtime instead.\n * Slices preserve the captured identity, so multipart uploads can read\n * disjoint ranges without materialising the whole file in memory or following\n * later leaf path swaps.\n */\nexport class FileSource implements ContentSource {\n /** Random-access: file ranges are read by absolute byte offset. */\n readonly canSlice = true\n /** File size captured at construction time. */\n readonly size: number\n private readonly path: FileSourcePath\n private readonly identity: FileIdentity\n\n /**\n * Create a FileSource for a local file path.\n * @param path - Local filesystem path or file URL.\n *\n * @throws If the runtime has no Node-compatible filesystem API.\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n constructor(path: FileSourcePath)\n /**\n * Internal constructor path for async validation.\n * @param path - Local filesystem path or file URL.\n * @param internal - Module-private validated identity payload.\n *\n * @internal\n */\n constructor(path: FileSourcePath, internal?: FileSourceInternalOptions) {\n const resolvedIdentity =\n internal?.key === FILE_SOURCE_INTERNAL\n ? internal.identity\n : validatedIdentityFromStats(path, getNodeFsSync().lstatSync(path))\n this.path = path\n this.identity = resolvedIdentity\n this.size = resolvedIdentity.size\n }\n\n /**\n * Return a new file-backed source covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return sliceFileRange(this.path, this.identity, 0, this.size, start, end)\n }\n\n /**\n * Open this file as a Web ReadableStream.\n * @returns A ReadableStream that reads the file lazily.\n */\n stream(): ReadableStream<Uint8Array> {\n return streamFileRange(this.path, this.identity, 0, this.size)\n }\n\n /**\n * Read this file into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise resolving with the file bytes.\n */\n toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n return fileRangeToArrayBuffer(this.path, this.identity, 0, this.size, options)\n }\n\n /**\n * Create a FileSource using asynchronous filesystem validation.\n * @param path - Local filesystem path or file URL.\n *\n * @returns A FileSource bound to the validated file identity.\n *\n * @throws If the path does not reference a regular non-symlink file.\n * @throws If a POSIX filesystem cannot report stable file identity.\n */\n static async fromPath(path: FileSourcePath): Promise<FileSource> {\n const identity = validatedIdentityFromStats(path, await lstatNodeFile(path))\n return constructFileSourceFromIdentity(path, identity)\n }\n}\n\nfunction constructFileSourceFromIdentity(path: FileSourcePath, identity: FileIdentity): FileSource {\n // The public overload is intentionally single-argument; Reflect.construct\n // lets fromPath pass the module-private validated identity without exposing\n // that constructor shape in the public type surface.\n return Reflect.construct(FileSource, [\n path,\n { key: FILE_SOURCE_INTERNAL, identity },\n ]) as FileSource\n}\n"],"mappings":"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"}
|
package/dist/streams/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_encryption = require("../types/encryption.cjs");
|
|
2
3
|
const require_hash = require("./hash.cjs");
|
|
3
4
|
const require_file_source = require("./file-source.cjs");
|
|
4
5
|
const require_source = require("./source.cjs");
|
|
5
|
-
const require_encryption = require("../types/encryption.cjs");
|
|
6
6
|
exports.BlobSource = require_source.BlobSource;
|
|
7
7
|
exports.BufferSource = require_source.BufferSource;
|
|
8
8
|
exports.EncryptionKey = require_encryption.EncryptionKey;
|
package/dist/streams/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { EncryptionKey } from "../types/encryption.js";
|
|
1
2
|
import { IncrementalSha1, sha1Hex } from "./hash.js";
|
|
2
3
|
import { FileSource } from "./file-source.js";
|
|
3
4
|
import { BlobSource, BufferSource, StreamSource, toContentSource } from "./source.js";
|
|
4
|
-
import { EncryptionKey } from "../types/encryption.js";
|
|
5
5
|
export { BlobSource, BufferSource, EncryptionKey, FileSource, IncrementalSha1, StreamSource, sha1Hex, toContentSource };
|
package/dist/streams/source.cjs
CHANGED
|
@@ -84,7 +84,8 @@ var BlobSource = class BlobSource {
|
|
|
84
84
|
async toArrayBuffer(options = {}) {
|
|
85
85
|
options.signal?.throwIfAborted();
|
|
86
86
|
if (options.signal === void 0) return this.blob.arrayBuffer();
|
|
87
|
-
|
|
87
|
+
const bytes = await require_collect.collectStream(this.stream(), options);
|
|
88
|
+
return require_bytes.arrayBufferFor(bytes);
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
91
|
/** ContentSource backed by a Uint8Array buffer. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.cjs","names":[],"sources":["../../src/streams/source.ts"],"sourcesContent":["import { arrayBufferFor } from '../util/bytes.ts'\nimport { collectStream } from './collect.ts'\n\nexport { FileSource, type FileSourcePath } from './file-source.ts'\n\nconst READABLE_STREAM_SIZE_REQUIRED_ERROR = 'size is required when using a ReadableStream as input.'\nconst FORWARD_ONLY_SIZE_REQUIRED_ERROR =\n 'size is required when using a forward-only content source as input.'\nconst STREAM_SOURCE_ENDED_EARLY_ERROR = 'StreamSource ended before the advertised byte count.'\nconst STREAM_SOURCE_TOO_MANY_BYTES_ERROR =\n 'StreamSource emitted more bytes than the advertised byte count.'\nconst STREAM_SOURCE_TOO_MANY_EMPTY_CHUNKS_ERROR =\n 'StreamSource emitted too many empty chunks without data.'\n/** Maximum consecutive empty chunks tolerated from a forward-only stream. */\nexport const MAX_EMPTY_STREAM_CHUNKS = 1024\n\nfunction asyncIterableToReadableStream(\n iterable: AsyncIterable<Uint8Array>,\n): ReadableStream<Uint8Array> {\n const iterator = iterable[Symbol.asyncIterator]()\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await iterator.next()\n if (done === true) {\n controller.close()\n return\n }\n if (!(value instanceof Uint8Array)) {\n throw new TypeError('Async iterable content sources must yield Uint8Array chunks.')\n }\n controller.enqueue(value)\n } catch (err) {\n /* v8 ignore next -- Iterator-return failure must not mask the pull error. */\n await returnAsyncIteratorBestEffort(iterator)\n throw err\n }\n },\n async cancel(reason) {\n await returnAsyncIteratorBestEffort(iterator, reason)\n },\n })\n}\n\nasync function returnAsyncIteratorBestEffort(\n iterator: AsyncIterator<Uint8Array>,\n reason?: unknown,\n): Promise<void> {\n try {\n await iterator.return?.(reason)\n } catch {\n // Iterator cleanup is secondary and must not mask the pull error.\n }\n}\n\nfunction isAsyncIterable(input: unknown): input is AsyncIterable<Uint8Array> {\n return (\n typeof input === 'object' &&\n input !== null &&\n Symbol.asyncIterator in input &&\n typeof input[Symbol.asyncIterator] === 'function'\n )\n}\n\nfunction isReadableStream(input: unknown): input is ReadableStream<Uint8Array> {\n return (\n typeof input === 'object' &&\n input !== null &&\n typeof (input as { getReader?: unknown }).getReader === 'function'\n )\n}\n\n/**\n * Uniform adapter for upload content. Wraps File, Blob, Buffer, local files,\n * async iterables, or ReadableStream behind a common interface so upload logic\n * does not depend on the input type.\n */\nexport interface ContentSource {\n /** Total size of the content in bytes. */\n readonly size: number\n /** Pre-computed SHA-1 hex digest, if available. */\n readonly sha1?: string\n /**\n * Whether {@link slice} is safe to call on this source.\n *\n * `true` for in-memory / random-access sources (`BufferSource`,\n * `BlobSource`, `FileSource`) — the multipart upload engine can dispatch\n * part reads in parallel by slicing the source into disjoint ranges.\n * `false` for forward-only streams and async iterables — the engine must\n * read sequentially, one `partSize` chunk at a time. Callers that branch on\n * this flag are expected to fall back to the sequential path rather than call\n * `slice()` and catch the throw.\n */\n readonly canSlice: boolean\n /** Return a sub-range of this source as a new ContentSource. */\n slice(start: number, end: number): ContentSource\n /** Open the content as a ReadableStream. */\n stream(): ReadableStream<Uint8Array>\n /** Read the entire content into an ArrayBuffer. */\n toArrayBuffer(options?: { readonly signal?: AbortSignal }): Promise<ArrayBuffer>\n}\n\n/** ContentSource backed by a Blob or File. */\nexport class BlobSource implements ContentSource {\n /** {@inheritDoc} */\n readonly size: number\n /** Random-access: `Blob.slice()` is cheap and returns a new Blob view. */\n readonly canSlice = true\n\n /**\n * Create a BlobSource wrapping the given Blob.\n * @param blob - The Blob or File to use as the underlying content.\n */\n constructor(private readonly blob: Blob) {\n this.size = blob.size\n }\n\n /**\n * Return a new BlobSource covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return new BlobSource(this.blob.slice(start, end))\n }\n\n /**\n * Open the Blob content as a ReadableStream.\n * @returns A ReadableStream of the Blob bytes.\n */\n stream(): ReadableStream<Uint8Array> {\n return this.blob.stream() as ReadableStream<Uint8Array>\n }\n\n /**\n * Read the entire Blob content into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise that resolves with the full content as an ArrayBuffer.\n */\n async toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n options.signal?.throwIfAborted()\n if (options.signal === undefined) return this.blob.arrayBuffer()\n const bytes = await collectStream(this.stream(), options)\n return arrayBufferFor(bytes)\n }\n}\n\n/** ContentSource backed by a Uint8Array buffer. */\nexport class BufferSource implements ContentSource {\n /** {@inheritDoc} */\n readonly size: number\n /** Random-access: the entire payload lives in memory. */\n readonly canSlice = true\n\n /**\n * Create a BufferSource wrapping the given Uint8Array.\n * @param buffer - The byte buffer to use as the underlying content.\n */\n constructor(private readonly buffer: Uint8Array) {\n this.size = buffer.byteLength\n }\n\n /**\n * Return a new BufferSource covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return new BufferSource(this.buffer.slice(start, end))\n }\n\n /**\n * Open the buffer content as a ReadableStream.\n * @returns A ReadableStream that emits the buffer bytes in a single chunk.\n */\n stream(): ReadableStream<Uint8Array> {\n const buffer = this.buffer\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buffer)\n controller.close()\n },\n })\n }\n\n /**\n * Read the entire buffer content into an ArrayBuffer.\n * @param options - Optional abort signal checked before returning bytes.\n *\n * @returns A promise that resolves with the full content as an ArrayBuffer.\n */\n async toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n options.signal?.throwIfAborted()\n return arrayBufferFor(this.buffer)\n }\n}\n\n/** ContentSource backed by a ReadableStream. Can only be consumed once and does not support slicing. */\nexport class StreamSource implements ContentSource {\n /** {@inheritDoc} */\n readonly size: number\n /**\n * Forward-only: ReadableStreams cannot be repositioned, so multipart\n * uploads must take the sequential path. See the interface comment on\n * `canSlice` for what the engine does with this flag.\n */\n readonly canSlice = false\n /** Whether the stream has already been read. */\n private consumed = false\n\n /**\n * Create a StreamSource wrapping the given ReadableStream with a known byte size.\n * @param readable - The ReadableStream to wrap as a content source.\n * @param size - The total number of bytes the stream will produce.\n */\n constructor(\n private readonly readable: ReadableStream<Uint8Array>,\n size: number,\n ) {\n validateStreamSourceSize(size)\n this.size = size\n }\n\n /**\n * Always throws because streams cannot be sliced. Buffer the stream first.\n *\n * @throws If slicing is attempted on a stream-backed source.\n */\n slice(): ContentSource {\n throw new Error('StreamSource does not support slicing. Buffer the stream first.')\n }\n\n /**\n * Open the underlying ReadableStream. Can only be called once.\n * @returns The underlying ReadableStream of bytes.\n *\n * @throws If the stream has already been consumed.\n */\n stream(): ReadableStream<Uint8Array> {\n if (this.consumed) throw new Error('StreamSource can only be consumed once.')\n this.consumed = true\n return this.readable\n }\n\n /**\n * Read the entire stream into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise that resolves with the full content as an ArrayBuffer.\n */\n async toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n const bytes = await collectStreamExactly(this.stream(), this.size, options.signal)\n return bytes.buffer as ArrayBuffer\n }\n}\n\nfunction validateStreamSourceSize(size: number): void {\n if (!Number.isFinite(size) || !Number.isInteger(size) || size < 0) {\n throw new RangeError('StreamSource size must be a non-negative finite integer.')\n }\n}\n\n/**\n * Reads exactly the advertised number of bytes from a stream.\n * @param stream - Stream to consume.\n * @param expectedSize - Exact number of bytes expected from the stream.\n * @param signal - Optional abort signal for cancelling the read.\n *\n * @returns A byte array of length `expectedSize`.\n *\n * @throws If the stream emits too few bytes, too many bytes, too many empty chunks, or aborts.\n */\nexport async function collectStreamExactly(\n stream: ReadableStream<Uint8Array>,\n expectedSize: number,\n signal?: AbortSignal,\n): Promise<Uint8Array<ArrayBuffer>> {\n const reader = stream.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n let completed = false\n\n try {\n while (total < expectedSize) {\n const { done, value } = await readNextNonEmptyStreamChunk(\n reader,\n STREAM_SOURCE_TOO_MANY_EMPTY_CHUNKS_ERROR,\n signal,\n )\n if (done) throw new Error(STREAM_SOURCE_ENDED_EARLY_ERROR)\n if (total + value.byteLength > expectedSize) {\n throw new Error(STREAM_SOURCE_TOO_MANY_BYTES_ERROR)\n }\n chunks.push(value)\n total += value.byteLength\n }\n\n const extra = await readNextNonEmptyStreamChunk(\n reader,\n STREAM_SOURCE_TOO_MANY_EMPTY_CHUNKS_ERROR,\n signal,\n )\n if (!extra.done) throw new Error(STREAM_SOURCE_TOO_MANY_BYTES_ERROR)\n\n const result = new Uint8Array(expectedSize)\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.byteLength\n }\n completed = true\n return result\n } finally {\n if (!completed) {\n cancelReaderBestEffort(reader)\n }\n try {\n reader.releaseLock()\n } catch {\n // Aborted reads can leave a pending read while best-effort cancel is\n // still unsettled. Preserve the original abort/error instead.\n }\n }\n}\n\n/**\n * Reads from a stream until it receives data, EOF, or too many consecutive empty chunks.\n * @param reader - Locked reader for a Uint8Array stream.\n * @param emptyChunkErrorMessage - Error message to throw when the empty-chunk limit is exceeded.\n * @param signal - Optional abort signal that cancels the reader and rejects the read.\n *\n * @returns The next non-empty chunk or EOF result.\n */\nexport async function readNextNonEmptyStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n emptyChunkErrorMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n let emptyChunks = 0\n while (true) {\n const result = await readStreamChunk(reader, signal)\n if (result.done || result.value.byteLength > 0) return result\n emptyChunks += 1\n if (emptyChunks > MAX_EMPTY_STREAM_CHUNKS) {\n throw new Error(emptyChunkErrorMessage)\n }\n }\n}\n\nasync function readStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n signal: AbortSignal | undefined,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n if (signal === undefined) return reader.read()\n if (signal.aborted) {\n const reason = signal.reason ?? new DOMException('Aborted', 'AbortError')\n cancelReaderBestEffort(reader, reason)\n throw reason\n }\n\n let removeAbortListener: (() => void) | undefined\n const abort = new Promise<never>((_, reject) => {\n const onAbort = (): void => {\n const reason = signal.reason ?? new DOMException('Aborted', 'AbortError')\n cancelReaderBestEffort(reader, reason)\n reject(reason)\n }\n signal.addEventListener('abort', onAbort, { once: true })\n removeAbortListener = () => signal.removeEventListener('abort', onAbort)\n })\n\n try {\n const result = await Promise.race([reader.read(), abort])\n if (signal.aborted) {\n const reason = signal.reason ?? new DOMException('Aborted', 'AbortError')\n cancelReaderBestEffort(reader, reason)\n throw reason\n }\n return result\n } finally {\n removeAbortListener?.()\n }\n}\n\nfunction cancelReaderBestEffort(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n reason?: unknown,\n): void {\n /* v8 ignore next -- Reader cancellation failure is deliberately best-effort. */\n void reader.cancel(reason).catch(() => {})\n}\n\n/** ContentSource backed by a forward-only async iterable of Uint8Array chunks. */\nclass AsyncIterableSource extends StreamSource {\n /**\n * Create an AsyncIterableSource from a known-size async iterable.\n * @param iterable - Async iterable that yields Uint8Array chunks.\n * @param size - Total byte length the iterable will produce.\n */\n constructor(iterable: AsyncIterable<Uint8Array>, size: number) {\n super(asyncIterableToReadableStream(iterable), size)\n }\n}\n\n/**\n * Convert a Uint8Array, Blob, ReadableStream, or async iterable into a {@link ContentSource}.\n * When passing a ReadableStream or async iterable, the `size` parameter is required.\n * @param input - The content to wrap.\n * @param size - The total byte length, required for forward-only inputs.\n *\n * @returns A ContentSource adapter for the given input.\n *\n * @throws If input is forward-only and size is not provided.\n */\nexport function toContentSource(\n input: Uint8Array | Blob | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,\n size?: number,\n): ContentSource {\n if (input instanceof Uint8Array) {\n return new BufferSource(input)\n }\n if (input instanceof Blob) {\n return new BlobSource(input)\n }\n if (isReadableStream(input)) {\n if (size === undefined) {\n throw new Error(READABLE_STREAM_SIZE_REQUIRED_ERROR)\n }\n return new StreamSource(input, size)\n }\n if (isAsyncIterable(input)) {\n if (size === undefined) {\n throw new Error(FORWARD_ONLY_SIZE_REQUIRED_ERROR)\n }\n return new AsyncIterableSource(input, size)\n }\n throw new TypeError('Unsupported content source input.')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"source.cjs","names":[],"sources":["../../src/streams/source.ts"],"sourcesContent":["import { arrayBufferFor } from '../util/bytes.ts'\nimport { collectStream } from './collect.ts'\n\nexport { FileSource, type FileSourcePath } from './file-source.ts'\n\nconst READABLE_STREAM_SIZE_REQUIRED_ERROR = 'size is required when using a ReadableStream as input.'\nconst FORWARD_ONLY_SIZE_REQUIRED_ERROR =\n 'size is required when using a forward-only content source as input.'\nconst STREAM_SOURCE_ENDED_EARLY_ERROR = 'StreamSource ended before the advertised byte count.'\nconst STREAM_SOURCE_TOO_MANY_BYTES_ERROR =\n 'StreamSource emitted more bytes than the advertised byte count.'\nconst STREAM_SOURCE_TOO_MANY_EMPTY_CHUNKS_ERROR =\n 'StreamSource emitted too many empty chunks without data.'\n/** Maximum consecutive empty chunks tolerated from a forward-only stream. */\nexport const MAX_EMPTY_STREAM_CHUNKS = 1024\n\nfunction asyncIterableToReadableStream(\n iterable: AsyncIterable<Uint8Array>,\n): ReadableStream<Uint8Array> {\n const iterator = iterable[Symbol.asyncIterator]()\n return new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await iterator.next()\n if (done === true) {\n controller.close()\n return\n }\n if (!(value instanceof Uint8Array)) {\n throw new TypeError('Async iterable content sources must yield Uint8Array chunks.')\n }\n controller.enqueue(value)\n } catch (err) {\n /* v8 ignore next -- Iterator-return failure must not mask the pull error. */\n await returnAsyncIteratorBestEffort(iterator)\n throw err\n }\n },\n async cancel(reason) {\n await returnAsyncIteratorBestEffort(iterator, reason)\n },\n })\n}\n\nasync function returnAsyncIteratorBestEffort(\n iterator: AsyncIterator<Uint8Array>,\n reason?: unknown,\n): Promise<void> {\n try {\n await iterator.return?.(reason)\n } catch {\n // Iterator cleanup is secondary and must not mask the pull error.\n }\n}\n\nfunction isAsyncIterable(input: unknown): input is AsyncIterable<Uint8Array> {\n return (\n typeof input === 'object' &&\n input !== null &&\n Symbol.asyncIterator in input &&\n typeof input[Symbol.asyncIterator] === 'function'\n )\n}\n\nfunction isReadableStream(input: unknown): input is ReadableStream<Uint8Array> {\n return (\n typeof input === 'object' &&\n input !== null &&\n typeof (input as { getReader?: unknown }).getReader === 'function'\n )\n}\n\n/**\n * Uniform adapter for upload content. Wraps File, Blob, Buffer, local files,\n * async iterables, or ReadableStream behind a common interface so upload logic\n * does not depend on the input type.\n */\nexport interface ContentSource {\n /** Total size of the content in bytes. */\n readonly size: number\n /** Pre-computed SHA-1 hex digest, if available. */\n readonly sha1?: string\n /**\n * Whether {@link slice} is safe to call on this source.\n *\n * `true` for in-memory / random-access sources (`BufferSource`,\n * `BlobSource`, `FileSource`) — the multipart upload engine can dispatch\n * part reads in parallel by slicing the source into disjoint ranges.\n * `false` for forward-only streams and async iterables — the engine must\n * read sequentially, one `partSize` chunk at a time. Callers that branch on\n * this flag are expected to fall back to the sequential path rather than call\n * `slice()` and catch the throw.\n */\n readonly canSlice: boolean\n /** Return a sub-range of this source as a new ContentSource. */\n slice(start: number, end: number): ContentSource\n /** Open the content as a ReadableStream. */\n stream(): ReadableStream<Uint8Array>\n /** Read the entire content into an ArrayBuffer. */\n toArrayBuffer(options?: { readonly signal?: AbortSignal }): Promise<ArrayBuffer>\n}\n\n/** ContentSource backed by a Blob or File. */\nexport class BlobSource implements ContentSource {\n /** {@inheritDoc} */\n readonly size: number\n /** Random-access: `Blob.slice()` is cheap and returns a new Blob view. */\n readonly canSlice = true\n\n /**\n * Create a BlobSource wrapping the given Blob.\n * @param blob - The Blob or File to use as the underlying content.\n */\n constructor(private readonly blob: Blob) {\n this.size = blob.size\n }\n\n /**\n * Return a new BlobSource covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return new BlobSource(this.blob.slice(start, end))\n }\n\n /**\n * Open the Blob content as a ReadableStream.\n * @returns A ReadableStream of the Blob bytes.\n */\n stream(): ReadableStream<Uint8Array> {\n return this.blob.stream() as ReadableStream<Uint8Array>\n }\n\n /**\n * Read the entire Blob content into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise that resolves with the full content as an ArrayBuffer.\n */\n async toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n options.signal?.throwIfAborted()\n if (options.signal === undefined) return this.blob.arrayBuffer()\n const bytes = await collectStream(this.stream(), options)\n return arrayBufferFor(bytes)\n }\n}\n\n/** ContentSource backed by a Uint8Array buffer. */\nexport class BufferSource implements ContentSource {\n /** {@inheritDoc} */\n readonly size: number\n /** Random-access: the entire payload lives in memory. */\n readonly canSlice = true\n\n /**\n * Create a BufferSource wrapping the given Uint8Array.\n * @param buffer - The byte buffer to use as the underlying content.\n */\n constructor(private readonly buffer: Uint8Array) {\n this.size = buffer.byteLength\n }\n\n /**\n * Return a new BufferSource covering the specified byte range.\n * @param start - The zero-based byte offset to begin the slice.\n * @param end - The exclusive byte offset where the slice ends.\n *\n * @returns A new ContentSource representing the requested sub-range.\n */\n slice(start: number, end: number): ContentSource {\n return new BufferSource(this.buffer.slice(start, end))\n }\n\n /**\n * Open the buffer content as a ReadableStream.\n * @returns A ReadableStream that emits the buffer bytes in a single chunk.\n */\n stream(): ReadableStream<Uint8Array> {\n const buffer = this.buffer\n return new ReadableStream({\n start(controller) {\n controller.enqueue(buffer)\n controller.close()\n },\n })\n }\n\n /**\n * Read the entire buffer content into an ArrayBuffer.\n * @param options - Optional abort signal checked before returning bytes.\n *\n * @returns A promise that resolves with the full content as an ArrayBuffer.\n */\n async toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n options.signal?.throwIfAborted()\n return arrayBufferFor(this.buffer)\n }\n}\n\n/** ContentSource backed by a ReadableStream. Can only be consumed once and does not support slicing. */\nexport class StreamSource implements ContentSource {\n /** {@inheritDoc} */\n readonly size: number\n /**\n * Forward-only: ReadableStreams cannot be repositioned, so multipart\n * uploads must take the sequential path. See the interface comment on\n * `canSlice` for what the engine does with this flag.\n */\n readonly canSlice = false\n /** Whether the stream has already been read. */\n private consumed = false\n\n /**\n * Create a StreamSource wrapping the given ReadableStream with a known byte size.\n * @param readable - The ReadableStream to wrap as a content source.\n * @param size - The total number of bytes the stream will produce.\n */\n constructor(\n private readonly readable: ReadableStream<Uint8Array>,\n size: number,\n ) {\n validateStreamSourceSize(size)\n this.size = size\n }\n\n /**\n * Always throws because streams cannot be sliced. Buffer the stream first.\n *\n * @throws If slicing is attempted on a stream-backed source.\n */\n slice(): ContentSource {\n throw new Error('StreamSource does not support slicing. Buffer the stream first.')\n }\n\n /**\n * Open the underlying ReadableStream. Can only be called once.\n * @returns The underlying ReadableStream of bytes.\n *\n * @throws If the stream has already been consumed.\n */\n stream(): ReadableStream<Uint8Array> {\n if (this.consumed) throw new Error('StreamSource can only be consumed once.')\n this.consumed = true\n return this.readable\n }\n\n /**\n * Read the entire stream into an ArrayBuffer.\n * @param options - Optional abort signal used while reading.\n *\n * @returns A promise that resolves with the full content as an ArrayBuffer.\n */\n async toArrayBuffer(options: { readonly signal?: AbortSignal } = {}): Promise<ArrayBuffer> {\n const bytes = await collectStreamExactly(this.stream(), this.size, options.signal)\n return bytes.buffer as ArrayBuffer\n }\n}\n\nfunction validateStreamSourceSize(size: number): void {\n if (!Number.isFinite(size) || !Number.isInteger(size) || size < 0) {\n throw new RangeError('StreamSource size must be a non-negative finite integer.')\n }\n}\n\n/**\n * Reads exactly the advertised number of bytes from a stream.\n * @param stream - Stream to consume.\n * @param expectedSize - Exact number of bytes expected from the stream.\n * @param signal - Optional abort signal for cancelling the read.\n *\n * @returns A byte array of length `expectedSize`.\n *\n * @throws If the stream emits too few bytes, too many bytes, too many empty chunks, or aborts.\n */\nexport async function collectStreamExactly(\n stream: ReadableStream<Uint8Array>,\n expectedSize: number,\n signal?: AbortSignal,\n): Promise<Uint8Array<ArrayBuffer>> {\n const reader = stream.getReader()\n const chunks: Uint8Array[] = []\n let total = 0\n let completed = false\n\n try {\n while (total < expectedSize) {\n const { done, value } = await readNextNonEmptyStreamChunk(\n reader,\n STREAM_SOURCE_TOO_MANY_EMPTY_CHUNKS_ERROR,\n signal,\n )\n if (done) throw new Error(STREAM_SOURCE_ENDED_EARLY_ERROR)\n if (total + value.byteLength > expectedSize) {\n throw new Error(STREAM_SOURCE_TOO_MANY_BYTES_ERROR)\n }\n chunks.push(value)\n total += value.byteLength\n }\n\n const extra = await readNextNonEmptyStreamChunk(\n reader,\n STREAM_SOURCE_TOO_MANY_EMPTY_CHUNKS_ERROR,\n signal,\n )\n if (!extra.done) throw new Error(STREAM_SOURCE_TOO_MANY_BYTES_ERROR)\n\n const result = new Uint8Array(expectedSize)\n let offset = 0\n for (const chunk of chunks) {\n result.set(chunk, offset)\n offset += chunk.byteLength\n }\n completed = true\n return result\n } finally {\n if (!completed) {\n cancelReaderBestEffort(reader)\n }\n try {\n reader.releaseLock()\n } catch {\n // Aborted reads can leave a pending read while best-effort cancel is\n // still unsettled. Preserve the original abort/error instead.\n }\n }\n}\n\n/**\n * Reads from a stream until it receives data, EOF, or too many consecutive empty chunks.\n * @param reader - Locked reader for a Uint8Array stream.\n * @param emptyChunkErrorMessage - Error message to throw when the empty-chunk limit is exceeded.\n * @param signal - Optional abort signal that cancels the reader and rejects the read.\n *\n * @returns The next non-empty chunk or EOF result.\n */\nexport async function readNextNonEmptyStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n emptyChunkErrorMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n let emptyChunks = 0\n while (true) {\n const result = await readStreamChunk(reader, signal)\n if (result.done || result.value.byteLength > 0) return result\n emptyChunks += 1\n if (emptyChunks > MAX_EMPTY_STREAM_CHUNKS) {\n throw new Error(emptyChunkErrorMessage)\n }\n }\n}\n\nasync function readStreamChunk(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n signal: AbortSignal | undefined,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n if (signal === undefined) return reader.read()\n if (signal.aborted) {\n const reason = signal.reason ?? new DOMException('Aborted', 'AbortError')\n cancelReaderBestEffort(reader, reason)\n throw reason\n }\n\n let removeAbortListener: (() => void) | undefined\n const abort = new Promise<never>((_, reject) => {\n const onAbort = (): void => {\n const reason = signal.reason ?? new DOMException('Aborted', 'AbortError')\n cancelReaderBestEffort(reader, reason)\n reject(reason)\n }\n signal.addEventListener('abort', onAbort, { once: true })\n removeAbortListener = () => signal.removeEventListener('abort', onAbort)\n })\n\n try {\n const result = await Promise.race([reader.read(), abort])\n if (signal.aborted) {\n const reason = signal.reason ?? new DOMException('Aborted', 'AbortError')\n cancelReaderBestEffort(reader, reason)\n throw reason\n }\n return result\n } finally {\n removeAbortListener?.()\n }\n}\n\nfunction cancelReaderBestEffort(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n reason?: unknown,\n): void {\n /* v8 ignore next -- Reader cancellation failure is deliberately best-effort. */\n void reader.cancel(reason).catch(() => {})\n}\n\n/** ContentSource backed by a forward-only async iterable of Uint8Array chunks. */\nclass AsyncIterableSource extends StreamSource {\n /**\n * Create an AsyncIterableSource from a known-size async iterable.\n * @param iterable - Async iterable that yields Uint8Array chunks.\n * @param size - Total byte length the iterable will produce.\n */\n constructor(iterable: AsyncIterable<Uint8Array>, size: number) {\n super(asyncIterableToReadableStream(iterable), size)\n }\n}\n\n/**\n * Convert a Uint8Array, Blob, ReadableStream, or async iterable into a {@link ContentSource}.\n * When passing a ReadableStream or async iterable, the `size` parameter is required.\n * @param input - The content to wrap.\n * @param size - The total byte length, required for forward-only inputs.\n *\n * @returns A ContentSource adapter for the given input.\n *\n * @throws If input is forward-only and size is not provided.\n */\nexport function toContentSource(\n input: Uint8Array | Blob | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,\n size?: number,\n): ContentSource {\n if (input instanceof Uint8Array) {\n return new BufferSource(input)\n }\n if (input instanceof Blob) {\n return new BlobSource(input)\n }\n if (isReadableStream(input)) {\n if (size === undefined) {\n throw new Error(READABLE_STREAM_SIZE_REQUIRED_ERROR)\n }\n return new StreamSource(input, size)\n }\n if (isAsyncIterable(input)) {\n if (size === undefined) {\n throw new Error(FORWARD_ONLY_SIZE_REQUIRED_ERROR)\n }\n return new AsyncIterableSource(input, size)\n }\n throw new TypeError('Unsupported content source input.')\n}\n"],"mappings":"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"}
|
package/dist/streams/source.js
CHANGED
|
@@ -84,7 +84,8 @@ var BlobSource = class BlobSource {
|
|
|
84
84
|
async toArrayBuffer(options = {}) {
|
|
85
85
|
options.signal?.throwIfAborted();
|
|
86
86
|
if (options.signal === void 0) return this.blob.arrayBuffer();
|
|
87
|
-
|
|
87
|
+
const bytes = await collectStream(this.stream(), options);
|
|
88
|
+
return arrayBufferFor(bytes);
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
91
|
/** ContentSource backed by a Uint8Array buffer. */
|