@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":"source.js","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.js","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,3 +1,4 @@
|
|
|
1
|
+
const require_abort = require("../util/abort.cjs");
|
|
1
2
|
const require_hash = require("../streams/hash.cjs");
|
|
2
3
|
const require_sha1_options = require("./sha1-options.cjs");
|
|
3
4
|
//#region src/sync/b2-sha1-reader.ts
|
|
@@ -26,7 +27,7 @@ async function readStreamChunkWithTimeout(reader, timeoutMillis, stalledMessage,
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
async function readRawStreamChunkWithTimeout(reader, timeoutMillis, stalledMessage, signal) {
|
|
29
|
-
signal
|
|
30
|
+
require_abort.throwIfSignalAborted(signal);
|
|
30
31
|
let timeout;
|
|
31
32
|
let removeAbortListener;
|
|
32
33
|
const readPromise = reader.read();
|
|
@@ -36,9 +37,12 @@ async function readRawStreamChunkWithTimeout(reader, timeoutMillis, stalledMessa
|
|
|
36
37
|
}, timeoutMillis);
|
|
37
38
|
});
|
|
38
39
|
const abortPromise = signal === void 0 ? void 0 : new Promise((_, reject) => {
|
|
39
|
-
const onAbort = () =>
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
reject(require_abort.abortReason(signal));
|
|
42
|
+
};
|
|
40
43
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
41
44
|
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
45
|
+
if (signal.aborted) onAbort();
|
|
42
46
|
});
|
|
43
47
|
try {
|
|
44
48
|
if (timeoutPromise === void 0 && abortPromise === void 0) return await readPromise;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"b2-sha1-reader.cjs","names":[],"sources":["../../src/sync/b2-sha1-reader.ts"],"sourcesContent":["import { IncrementalSha1 } from '../streams/hash.ts'\nimport { normalizeSha1TimeoutMillis } from './sha1-options.ts'\n\nconst MAX_CONSECUTIVE_EMPTY_READ_CHUNKS = 1024\n\n/**\n * Reads one non-empty stream chunk with an idle timeout and optional abort signal.\n * Empty chunks are not progress; too many consecutive empty chunks fail with the\n * same stalled-read diagnostic used for pending reads.\n *\n * @param reader - Locked reader to read from.\n * @param timeoutMillis - Idle timeout in milliseconds for this read.\n * @param stalledMessage - Error message used when the read makes no progress.\n * @param signal - Optional abort signal to observe while reading.\n *\n * @returns The next stream read result.\n *\n * @internal\n */\nexport async function readStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n let emptyChunks = 0\n while (true) {\n const result = await readRawStreamChunkWithTimeout(\n reader,\n timeoutMillis,\n stalledMessage,\n signal,\n )\n if (result.done || result.value.byteLength > 0) return result\n emptyChunks += 1\n if (emptyChunks > MAX_CONSECUTIVE_EMPTY_READ_CHUNKS) {\n throw new Error(stalledMessage)\n }\n }\n}\n\nasync function readRawStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n signal
|
|
1
|
+
{"version":3,"file":"b2-sha1-reader.cjs","names":[],"sources":["../../src/sync/b2-sha1-reader.ts"],"sourcesContent":["import { IncrementalSha1 } from '../streams/hash.ts'\nimport { abortReason, throwIfSignalAborted } from '../util/abort.ts'\nimport { normalizeSha1TimeoutMillis } from './sha1-options.ts'\n\nconst MAX_CONSECUTIVE_EMPTY_READ_CHUNKS = 1024\n\n/**\n * Reads one non-empty stream chunk with an idle timeout and optional abort signal.\n * Empty chunks are not progress; too many consecutive empty chunks fail with the\n * same stalled-read diagnostic used for pending reads.\n *\n * @param reader - Locked reader to read from.\n * @param timeoutMillis - Idle timeout in milliseconds for this read.\n * @param stalledMessage - Error message used when the read makes no progress.\n * @param signal - Optional abort signal to observe while reading.\n *\n * @returns The next stream read result.\n *\n * @internal\n */\nexport async function readStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n let emptyChunks = 0\n while (true) {\n const result = await readRawStreamChunkWithTimeout(\n reader,\n timeoutMillis,\n stalledMessage,\n signal,\n )\n if (result.done || result.value.byteLength > 0) return result\n emptyChunks += 1\n if (emptyChunks > MAX_CONSECUTIVE_EMPTY_READ_CHUNKS) {\n throw new Error(stalledMessage)\n }\n }\n}\n\nasync function readRawStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n throwIfSignalAborted(signal)\n\n let timeout: ReturnType<typeof setTimeout> | undefined\n let removeAbortListener: (() => void) | undefined\n const readPromise = reader.read()\n const timeoutPromise =\n timeoutMillis === Number.POSITIVE_INFINITY\n ? undefined\n : new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n reject(new Error(stalledMessage))\n }, timeoutMillis)\n })\n const abortPromise =\n signal === undefined\n ? undefined\n : new Promise<never>((_, reject) => {\n const onAbort = () => {\n reject(abortReason(signal))\n }\n signal.addEventListener('abort', onAbort, { once: true })\n removeAbortListener = () => signal.removeEventListener('abort', onAbort)\n if (signal.aborted) onAbort()\n })\n\n try {\n if (timeoutPromise === undefined && abortPromise === undefined) return await readPromise\n const candidates = [readPromise]\n if (timeoutPromise !== undefined) candidates.push(timeoutPromise)\n if (abortPromise !== undefined) candidates.push(abortPromise)\n return await Promise.race(candidates)\n } finally {\n if (timeout !== undefined) clearTimeout(timeout)\n removeAbortListener?.()\n void readPromise.catch(() => {})\n }\n}\n\n/**\n * Hashes a B2 response body as SHA-1 with idle timeout, abort, and size checks.\n *\n * @param body - Response body stream to hash.\n * @param signal - Optional abort signal to observe while reading.\n * @param options - Optional timeout and byte-count limits.\n *\n * @returns The computed SHA-1 and number of bytes read.\n *\n * @internal\n */\nexport async function hashReadableStreamSha1(\n body: ReadableStream<Uint8Array>,\n signal?: AbortSignal,\n options?: {\n readonly idleTimeoutMillis: number\n readonly maxBytes: number\n readonly expectedBytes: number\n },\n): Promise<{ readonly contentSha1: string; readonly bytesRead: number }> {\n const hash = new IncrementalSha1()\n const reader = body.getReader()\n const idleTimeoutMillis = options?.idleTimeoutMillis ?? normalizeSha1TimeoutMillis(undefined)\n const maxBytes = options?.maxBytes ?? Number.POSITIVE_INFINITY\n const expectedBytes = options?.expectedBytes\n let bytesRead = 0\n try {\n while (true) {\n const { done, value } = await readStreamChunkWithTimeout(\n reader,\n idleTimeoutMillis,\n `sha1 B2 read stalled for ${idleTimeoutMillis} ms`,\n signal,\n )\n if (done) break\n bytesRead += value.byteLength\n if (bytesRead > maxBytes) {\n throw new Error(`sha1 B2 read exceeded ${maxBytes} byte verification budget`)\n }\n await hash.update(value)\n }\n if (expectedBytes !== undefined && bytesRead !== expectedBytes) {\n throw new Error(`sha1 B2 read ended after ${bytesRead} bytes, expected ${expectedBytes}`)\n }\n return { contentSha1: await hash.digest(), bytesRead }\n } catch (err) {\n void reader.cancel(err).catch(() => {})\n throw err\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Applies an absolute verification deadline while forwarding parent aborts.\n *\n * @param signal - Optional parent abort signal.\n * @param timeoutMillis - Absolute deadline in milliseconds.\n * @param run - Operation to run with the derived deadline signal.\n *\n * @returns The operation result.\n *\n * @internal\n */\nexport async function withSha1VerificationDeadline<T>(\n signal: AbortSignal | undefined,\n timeoutMillis: number,\n run: (signal: AbortSignal) => Promise<T>,\n): Promise<T> {\n const controller = new AbortController()\n const abortFromParent = () => controller.abort(signal?.reason)\n if (signal?.aborted) abortFromParent()\n signal?.addEventListener('abort', abortFromParent, { once: true })\n\n let timeout: ReturnType<typeof setTimeout> | undefined\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n const error = new Error(`sha1 B2 verification exceeded ${timeoutMillis} ms`)\n controller.abort(error)\n reject(error)\n }, timeoutMillis)\n })\n const runPromise = run(controller.signal)\n try {\n return await Promise.race([runPromise, timeoutPromise])\n } finally {\n if (timeout !== undefined) clearTimeout(timeout)\n signal?.removeEventListener('abort', abortFromParent)\n void runPromise.catch(() => {})\n }\n}\n\n/**\n * Bounds B2 SHA-1 verification downloads to the selected object's size and optional ceiling.\n *\n * @param contentLength - Selected B2 object byte length.\n * @param ceiling - Optional lower verification budget.\n *\n * @returns The byte budget to enforce.\n *\n * @internal\n */\nexport function normalizeSha1VerificationMaxBytes(\n contentLength: number,\n ceiling: number | undefined,\n): number {\n const contentBudget = Math.max(0, Math.floor(contentLength))\n if (ceiling === undefined) return contentBudget\n if (!Number.isFinite(ceiling) || ceiling < 0) return contentBudget\n return Math.min(contentBudget, Math.floor(ceiling))\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"b2-sha1-reader.d.ts","sourceRoot":"","sources":["../../src/sync/b2-sha1-reader.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"b2-sha1-reader.d.ts","sourceRoot":"","sources":["../../src/sync/b2-sha1-reader.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,2BAA2B,CAAC,UAAU,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC,CAe/C;AA8CD;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B,GACA,OAAO,CAAC;IAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCvE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GACvC,OAAO,CAAC,CAAC,CAAC,CAsBZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAC/C,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,MAAM,CAKR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"b2-sha1-reader.d.ts","sourceRoot":"","sources":["../../src/sync/b2-sha1-reader.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"b2-sha1-reader.d.ts","sourceRoot":"","sources":["../../src/sync/b2-sha1-reader.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,2BAA2B,CAAC,UAAU,CAAC,EAC/C,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,wBAAwB,CAAC,UAAU,CAAC,CAAC,CAe/C;AA8CD;;;;;;;;;;GAUG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;CAC/B,GACA,OAAO,CAAC;IAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAgCvE;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GACvC,OAAO,CAAC,CAAC,CAAC,CAsBZ;AAED;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAC/C,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GAAG,SAAS,GAC1B,MAAM,CAKR"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { abortReason, throwIfSignalAborted } from "../util/abort.js";
|
|
1
2
|
import { IncrementalSha1 } from "../streams/hash.js";
|
|
2
3
|
import { normalizeSha1TimeoutMillis } from "./sha1-options.js";
|
|
3
4
|
//#region src/sync/b2-sha1-reader.ts
|
|
@@ -26,7 +27,7 @@ async function readStreamChunkWithTimeout(reader, timeoutMillis, stalledMessage,
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
async function readRawStreamChunkWithTimeout(reader, timeoutMillis, stalledMessage, signal) {
|
|
29
|
-
signal
|
|
30
|
+
throwIfSignalAborted(signal);
|
|
30
31
|
let timeout;
|
|
31
32
|
let removeAbortListener;
|
|
32
33
|
const readPromise = reader.read();
|
|
@@ -36,9 +37,12 @@ async function readRawStreamChunkWithTimeout(reader, timeoutMillis, stalledMessa
|
|
|
36
37
|
}, timeoutMillis);
|
|
37
38
|
});
|
|
38
39
|
const abortPromise = signal === void 0 ? void 0 : new Promise((_, reject) => {
|
|
39
|
-
const onAbort = () =>
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
reject(abortReason(signal));
|
|
42
|
+
};
|
|
40
43
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
41
44
|
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
45
|
+
if (signal.aborted) onAbort();
|
|
42
46
|
});
|
|
43
47
|
try {
|
|
44
48
|
if (timeoutPromise === void 0 && abortPromise === void 0) return await readPromise;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"b2-sha1-reader.js","names":[],"sources":["../../src/sync/b2-sha1-reader.ts"],"sourcesContent":["import { IncrementalSha1 } from '../streams/hash.ts'\nimport { normalizeSha1TimeoutMillis } from './sha1-options.ts'\n\nconst MAX_CONSECUTIVE_EMPTY_READ_CHUNKS = 1024\n\n/**\n * Reads one non-empty stream chunk with an idle timeout and optional abort signal.\n * Empty chunks are not progress; too many consecutive empty chunks fail with the\n * same stalled-read diagnostic used for pending reads.\n *\n * @param reader - Locked reader to read from.\n * @param timeoutMillis - Idle timeout in milliseconds for this read.\n * @param stalledMessage - Error message used when the read makes no progress.\n * @param signal - Optional abort signal to observe while reading.\n *\n * @returns The next stream read result.\n *\n * @internal\n */\nexport async function readStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n let emptyChunks = 0\n while (true) {\n const result = await readRawStreamChunkWithTimeout(\n reader,\n timeoutMillis,\n stalledMessage,\n signal,\n )\n if (result.done || result.value.byteLength > 0) return result\n emptyChunks += 1\n if (emptyChunks > MAX_CONSECUTIVE_EMPTY_READ_CHUNKS) {\n throw new Error(stalledMessage)\n }\n }\n}\n\nasync function readRawStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n signal
|
|
1
|
+
{"version":3,"file":"b2-sha1-reader.js","names":[],"sources":["../../src/sync/b2-sha1-reader.ts"],"sourcesContent":["import { IncrementalSha1 } from '../streams/hash.ts'\nimport { abortReason, throwIfSignalAborted } from '../util/abort.ts'\nimport { normalizeSha1TimeoutMillis } from './sha1-options.ts'\n\nconst MAX_CONSECUTIVE_EMPTY_READ_CHUNKS = 1024\n\n/**\n * Reads one non-empty stream chunk with an idle timeout and optional abort signal.\n * Empty chunks are not progress; too many consecutive empty chunks fail with the\n * same stalled-read diagnostic used for pending reads.\n *\n * @param reader - Locked reader to read from.\n * @param timeoutMillis - Idle timeout in milliseconds for this read.\n * @param stalledMessage - Error message used when the read makes no progress.\n * @param signal - Optional abort signal to observe while reading.\n *\n * @returns The next stream read result.\n *\n * @internal\n */\nexport async function readStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n let emptyChunks = 0\n while (true) {\n const result = await readRawStreamChunkWithTimeout(\n reader,\n timeoutMillis,\n stalledMessage,\n signal,\n )\n if (result.done || result.value.byteLength > 0) return result\n emptyChunks += 1\n if (emptyChunks > MAX_CONSECUTIVE_EMPTY_READ_CHUNKS) {\n throw new Error(stalledMessage)\n }\n }\n}\n\nasync function readRawStreamChunkWithTimeout(\n reader: ReadableStreamDefaultReader<Uint8Array>,\n timeoutMillis: number,\n stalledMessage: string,\n signal?: AbortSignal,\n): Promise<ReadableStreamReadResult<Uint8Array>> {\n throwIfSignalAborted(signal)\n\n let timeout: ReturnType<typeof setTimeout> | undefined\n let removeAbortListener: (() => void) | undefined\n const readPromise = reader.read()\n const timeoutPromise =\n timeoutMillis === Number.POSITIVE_INFINITY\n ? undefined\n : new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n reject(new Error(stalledMessage))\n }, timeoutMillis)\n })\n const abortPromise =\n signal === undefined\n ? undefined\n : new Promise<never>((_, reject) => {\n const onAbort = () => {\n reject(abortReason(signal))\n }\n signal.addEventListener('abort', onAbort, { once: true })\n removeAbortListener = () => signal.removeEventListener('abort', onAbort)\n if (signal.aborted) onAbort()\n })\n\n try {\n if (timeoutPromise === undefined && abortPromise === undefined) return await readPromise\n const candidates = [readPromise]\n if (timeoutPromise !== undefined) candidates.push(timeoutPromise)\n if (abortPromise !== undefined) candidates.push(abortPromise)\n return await Promise.race(candidates)\n } finally {\n if (timeout !== undefined) clearTimeout(timeout)\n removeAbortListener?.()\n void readPromise.catch(() => {})\n }\n}\n\n/**\n * Hashes a B2 response body as SHA-1 with idle timeout, abort, and size checks.\n *\n * @param body - Response body stream to hash.\n * @param signal - Optional abort signal to observe while reading.\n * @param options - Optional timeout and byte-count limits.\n *\n * @returns The computed SHA-1 and number of bytes read.\n *\n * @internal\n */\nexport async function hashReadableStreamSha1(\n body: ReadableStream<Uint8Array>,\n signal?: AbortSignal,\n options?: {\n readonly idleTimeoutMillis: number\n readonly maxBytes: number\n readonly expectedBytes: number\n },\n): Promise<{ readonly contentSha1: string; readonly bytesRead: number }> {\n const hash = new IncrementalSha1()\n const reader = body.getReader()\n const idleTimeoutMillis = options?.idleTimeoutMillis ?? normalizeSha1TimeoutMillis(undefined)\n const maxBytes = options?.maxBytes ?? Number.POSITIVE_INFINITY\n const expectedBytes = options?.expectedBytes\n let bytesRead = 0\n try {\n while (true) {\n const { done, value } = await readStreamChunkWithTimeout(\n reader,\n idleTimeoutMillis,\n `sha1 B2 read stalled for ${idleTimeoutMillis} ms`,\n signal,\n )\n if (done) break\n bytesRead += value.byteLength\n if (bytesRead > maxBytes) {\n throw new Error(`sha1 B2 read exceeded ${maxBytes} byte verification budget`)\n }\n await hash.update(value)\n }\n if (expectedBytes !== undefined && bytesRead !== expectedBytes) {\n throw new Error(`sha1 B2 read ended after ${bytesRead} bytes, expected ${expectedBytes}`)\n }\n return { contentSha1: await hash.digest(), bytesRead }\n } catch (err) {\n void reader.cancel(err).catch(() => {})\n throw err\n } finally {\n reader.releaseLock()\n }\n}\n\n/**\n * Applies an absolute verification deadline while forwarding parent aborts.\n *\n * @param signal - Optional parent abort signal.\n * @param timeoutMillis - Absolute deadline in milliseconds.\n * @param run - Operation to run with the derived deadline signal.\n *\n * @returns The operation result.\n *\n * @internal\n */\nexport async function withSha1VerificationDeadline<T>(\n signal: AbortSignal | undefined,\n timeoutMillis: number,\n run: (signal: AbortSignal) => Promise<T>,\n): Promise<T> {\n const controller = new AbortController()\n const abortFromParent = () => controller.abort(signal?.reason)\n if (signal?.aborted) abortFromParent()\n signal?.addEventListener('abort', abortFromParent, { once: true })\n\n let timeout: ReturnType<typeof setTimeout> | undefined\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n const error = new Error(`sha1 B2 verification exceeded ${timeoutMillis} ms`)\n controller.abort(error)\n reject(error)\n }, timeoutMillis)\n })\n const runPromise = run(controller.signal)\n try {\n return await Promise.race([runPromise, timeoutPromise])\n } finally {\n if (timeout !== undefined) clearTimeout(timeout)\n signal?.removeEventListener('abort', abortFromParent)\n void runPromise.catch(() => {})\n }\n}\n\n/**\n * Bounds B2 SHA-1 verification downloads to the selected object's size and optional ceiling.\n *\n * @param contentLength - Selected B2 object byte length.\n * @param ceiling - Optional lower verification budget.\n *\n * @returns The byte budget to enforce.\n *\n * @internal\n */\nexport function normalizeSha1VerificationMaxBytes(\n contentLength: number,\n ceiling: number | undefined,\n): number {\n const contentBudget = Math.max(0, Math.floor(contentLength))\n if (ceiling === undefined) return contentBudget\n if (!Number.isFinite(ceiling) || ceiling < 0) return contentBudget\n return Math.min(contentBudget, Math.floor(ceiling))\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ var DOWNLOAD_STAGING_DIRECTORY_NAME = ".b2sdk-download-staging";
|
|
|
6
6
|
var DOWNLOAD_STAGING_MARKER_NAME = ".b2sdk-staging-marker.partial";
|
|
7
7
|
var CANONICAL_DOWNLOAD_STAGING_DIRECTORY_NAME = canonicalLocalFilesystemSegment(DOWNLOAD_STAGING_DIRECTORY_NAME);
|
|
8
8
|
var DOWNLOAD_STAGING_ENTRY_SUFFIX = ".download";
|
|
9
|
-
var STALE_DOWNLOAD_STAGING_AGE_MS =
|
|
9
|
+
var STALE_DOWNLOAD_STAGING_AGE_MS = 864e5;
|
|
10
10
|
var MAX_STAGING_CLEANUP_CONCURRENCY = 8;
|
|
11
11
|
var MAX_CLEANUP_WARNING_ENTRIES = 3;
|
|
12
12
|
/** @internal */
|
|
@@ -31,12 +31,13 @@ function isDownloadStagingDirectorySegment(segment) {
|
|
|
31
31
|
* @param randomUUID - UUID provider used to create unique staging entries.
|
|
32
32
|
* @param statForDeviceCheck - Stat function used to verify filesystem devices.
|
|
33
33
|
* @param beforeStagingMarkerWrite - Test hook called before marker creation.
|
|
34
|
+
* @param activityEntryLimit - Maximum entry count inspected for stale staging activity.
|
|
34
35
|
*
|
|
35
36
|
* @returns The resolved staging directory path.
|
|
36
37
|
*
|
|
37
38
|
* @internal
|
|
38
39
|
*/
|
|
39
|
-
async function createDownloadStagingDirectory(rootRealPath, path, randomUUID, statForDeviceCheck, beforeStagingMarkerWrite) {
|
|
40
|
+
async function createDownloadStagingDirectory(rootRealPath, path, randomUUID, statForDeviceCheck, beforeStagingMarkerWrite, activityEntryLimit = DOWNLOAD_STAGING_ACTIVITY_ENTRY_LIMIT) {
|
|
40
41
|
const { chmod, lstat, mkdir, readdir, realpath, rm } = await import("node:fs/promises");
|
|
41
42
|
const managedDirectory = path.join(rootRealPath, DOWNLOAD_STAGING_DIRECTORY_NAME);
|
|
42
43
|
try {
|
|
@@ -55,7 +56,7 @@ async function createDownloadStagingDirectory(rootRealPath, path, randomUUID, st
|
|
|
55
56
|
await writeStagingMarker(realManagedDirectory, path);
|
|
56
57
|
/* v8 ignore next -- best-effort chmod */
|
|
57
58
|
await chmod(realManagedDirectory, PRIVATE_DOWNLOAD_DIRECTORY_MODE).catch(() => {});
|
|
58
|
-
await reapStaleDownloadStagingDirectoriesOnce(realManagedDirectory, path, Date.now());
|
|
59
|
+
await reapStaleDownloadStagingDirectoriesOnce(realManagedDirectory, path, Date.now(), activityEntryLimit);
|
|
59
60
|
const stagingDirectory = path.join(realManagedDirectory, `${Date.now()}-${randomUUID()}${DOWNLOAD_STAGING_ENTRY_SUFFIX}`);
|
|
60
61
|
await mkdir(stagingDirectory, { mode: PRIVATE_DOWNLOAD_DIRECTORY_MODE });
|
|
61
62
|
/* v8 ignore next -- best-effort chmod */
|
|
@@ -141,19 +142,19 @@ async function assertDownloadPathSameDevice(rootRealPath, candidateRealPath, sta
|
|
|
141
142
|
const [rootStats, candidateStats] = await Promise.all([statForDeviceCheck(rootRealPath), statForDeviceCheck(candidateRealPath)]);
|
|
142
143
|
if (rootStats.dev !== candidateStats.dev) throw new Error(message);
|
|
143
144
|
}
|
|
144
|
-
async function reapStaleDownloadStagingDirectoriesOnce(managedDirectory, path, nowMillis) {
|
|
145
|
+
async function reapStaleDownloadStagingDirectoriesOnce(managedDirectory, path, nowMillis, activityEntryLimit) {
|
|
145
146
|
const previous = reapedManagedDirectories.get(managedDirectory);
|
|
146
147
|
if (previous !== void 0) {
|
|
147
148
|
await previous;
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
|
-
const next = reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis).finally(() => {
|
|
151
|
+
const next = reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis, activityEntryLimit).finally(() => {
|
|
151
152
|
if (reapedManagedDirectories.get(managedDirectory) === next) reapedManagedDirectories.delete(managedDirectory);
|
|
152
153
|
});
|
|
153
154
|
reapedManagedDirectories.set(managedDirectory, next);
|
|
154
155
|
await next;
|
|
155
156
|
}
|
|
156
|
-
async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis) {
|
|
157
|
+
async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis, activityEntryLimit) {
|
|
157
158
|
const { readdir, realpath, rm } = await import("node:fs/promises");
|
|
158
159
|
let entries;
|
|
159
160
|
try {
|
|
@@ -167,7 +168,7 @@ async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMi
|
|
|
167
168
|
await forEachWithConcurrency(entries, MAX_STAGING_CLEANUP_CONCURRENCY, async (entry) => {
|
|
168
169
|
if (!entry.isDirectory() || !entry.name.endsWith(DOWNLOAD_STAGING_ENTRY_SUFFIX)) return;
|
|
169
170
|
const candidate = path.join(managedDirectory, entry.name);
|
|
170
|
-
const activity = await readManagedStagingEntryActivity(candidate, path);
|
|
171
|
+
const activity = await readManagedStagingEntryActivity(candidate, path, activityEntryLimit);
|
|
171
172
|
if (activity === void 0 || !stagingActivityIsStale(activity, nowMillis)) return;
|
|
172
173
|
const realCandidate = await realpath(candidate).catch(() => {
|
|
173
174
|
cleanupErrors.push({
|
|
@@ -178,7 +179,7 @@ async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMi
|
|
|
178
179
|
if (realCandidate === void 0) return;
|
|
179
180
|
try {
|
|
180
181
|
require_path_safety.assertPathInsideRoot(managedDirectory, realCandidate, path);
|
|
181
|
-
const latestActivity = await readManagedStagingEntryActivity(candidate, path);
|
|
182
|
+
const latestActivity = await readManagedStagingEntryActivity(candidate, path, activityEntryLimit);
|
|
182
183
|
if (latestActivity === void 0 || latestActivity.signature !== activity.signature || !stagingActivityIsStale(latestActivity, nowMillis)) return;
|
|
183
184
|
await rm(realCandidate, {
|
|
184
185
|
recursive: true,
|
|
@@ -196,13 +197,13 @@ async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMi
|
|
|
196
197
|
emitCleanupWarning(`failed to reap ${cleanupErrors.length} stale B2 SDK download staging ${noun}: ${cleanupErrors.slice(0, MAX_CLEANUP_WARNING_ENTRIES).map(formatCleanupWarning).join("; ")}`);
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
|
-
async function readManagedStagingEntryActivity(candidate, path) {
|
|
200
|
+
async function readManagedStagingEntryActivity(candidate, path, activityEntryLimit) {
|
|
200
201
|
const { lstat, readdir } = await import("node:fs/promises");
|
|
201
202
|
try {
|
|
202
203
|
const [directoryStats, markerStats] = await Promise.all([lstat(candidate), lstat(path.join(candidate, DOWNLOAD_STAGING_MARKER_NAME))]);
|
|
203
204
|
if (!directoryStats.isDirectory() || !markerStats.isFile()) return void 0;
|
|
204
205
|
const entries = await readdir(candidate, { withFileTypes: true });
|
|
205
|
-
if (entries.length >
|
|
206
|
+
if (entries.length > activityEntryLimit) return void 0;
|
|
206
207
|
const statsParts = [`.:${stagingStatsSignature(directoryStats)}`, `${DOWNLOAD_STAGING_MARKER_NAME}:${stagingStatsSignature(markerStats)}`];
|
|
207
208
|
let newestActivityMs = Math.max(stagingStatsActivityMs(directoryStats), stagingStatsActivityMs(markerStats));
|
|
208
209
|
for (const entry of [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download-staging.cjs","names":[],"sources":["../../src/sync/download-staging.ts"],"sourcesContent":["import { assertPathInsideRoot, hasErrorCode, noFollowFlag } from './path-safety.ts'\n\ntype DeviceStat = { readonly dev: number | bigint }\ntype DeviceStatFn = (path: string) => Promise<DeviceStat>\n\n/** @internal */\nexport const DOWNLOAD_STAGING_DIRECTORY_NAME = '.b2sdk-download-staging'\n/** @internal */\nexport const DOWNLOAD_STAGING_MARKER_NAME = '.b2sdk-staging-marker.partial'\n\nconst CANONICAL_DOWNLOAD_STAGING_DIRECTORY_NAME = canonicalLocalFilesystemSegment(\n DOWNLOAD_STAGING_DIRECTORY_NAME,\n)\nconst DOWNLOAD_STAGING_ENTRY_SUFFIX = '.download'\nconst STALE_DOWNLOAD_STAGING_AGE_MS = 24 * 60 * 60 * 1000\nconst MAX_STAGING_CLEANUP_CONCURRENCY = 8\nconst MAX_CLEANUP_WARNING_ENTRIES = 3\n/** @internal */\nexport const DOWNLOAD_STAGING_ACTIVITY_ENTRY_LIMIT = 1024\nconst reapedManagedDirectories = new Map<string, Promise<void>>()\n\ntype StagingActivitySnapshot = {\n readonly newestActivityMs: number\n readonly signature: string\n}\n\ntype CleanupWarning = {\n readonly entryName: string\n readonly operation: 'inspect' | 'remove'\n}\n\n/**\n * Checks whether a single path segment matches the SDK-managed staging directory name.\n * @param segment - Candidate path segment.\n *\n * @returns True when the segment is the reserved staging directory name under\n * local filesystem canonicalization.\n *\n * @internal\n */\nexport function isDownloadStagingDirectorySegment(segment: string | undefined): boolean {\n return (\n segment !== undefined &&\n canonicalLocalFilesystemSegment(segment) === CANONICAL_DOWNLOAD_STAGING_DIRECTORY_NAME\n )\n}\n\n/**\n * Creates a private SDK-managed staging directory under a local sync root.\n * @param rootRealPath - Resolved local sync root path.\n * @param path - Node path module used for platform-specific path operations.\n * @param randomUUID - UUID provider used to create unique staging entries.\n * @param statForDeviceCheck - Stat function used to verify filesystem devices.\n * @param beforeStagingMarkerWrite - Test hook called before marker creation.\n *\n * @returns The resolved staging directory path.\n *\n * @internal\n */\nexport async function createDownloadStagingDirectory(\n rootRealPath: string,\n path: typeof import('node:path'),\n randomUUID: () => string,\n statForDeviceCheck: DeviceStatFn,\n beforeStagingMarkerWrite?: (directory: string) => Promise<void> | void,\n): Promise<string> {\n const { chmod, lstat, mkdir, readdir, realpath, rm } = await import('node:fs/promises')\n const managedDirectory = path.join(rootRealPath, DOWNLOAD_STAGING_DIRECTORY_NAME)\n try {\n await mkdir(managedDirectory, { mode: PRIVATE_DOWNLOAD_DIRECTORY_MODE })\n } catch (err) {\n if (!hasErrorCode(err, 'EEXIST')) throw err\n }\n const managedStats = await lstat(managedDirectory)\n if (!managedStats.isDirectory()) {\n throw new Error(\n `unsafe local destination path: ${DOWNLOAD_STAGING_DIRECTORY_NAME} is not a directory`,\n )\n }\n const realManagedDirectory = await realpath(managedDirectory)\n assertPathInsideRoot(rootRealPath, realManagedDirectory, path)\n await assertDownloadPathSameDevice(\n rootRealPath,\n realManagedDirectory,\n statForDeviceCheck,\n 'unsafe local destination path: cannot stage download across filesystems',\n )\n\n if (!(await isManagedDownloadStagingRoot(realManagedDirectory))) {\n const entries = await readdir(realManagedDirectory)\n if (entries.length > 0 && !(await isManagedDownloadStagingRoot(realManagedDirectory))) {\n throw new Error(\n `unsafe local destination path: ${DOWNLOAD_STAGING_DIRECTORY_NAME} is reserved for SDK download staging`,\n )\n }\n }\n await beforeStagingMarkerWrite?.(realManagedDirectory)\n await writeStagingMarker(realManagedDirectory, path)\n /* v8 ignore next -- best-effort chmod */\n await chmod(realManagedDirectory, PRIVATE_DOWNLOAD_DIRECTORY_MODE).catch(() => {})\n await reapStaleDownloadStagingDirectoriesOnce(realManagedDirectory, path, Date.now())\n\n const stagingDirectory = path.join(\n realManagedDirectory,\n `${Date.now()}-${randomUUID()}${DOWNLOAD_STAGING_ENTRY_SUFFIX}`,\n )\n await mkdir(stagingDirectory, { mode: PRIVATE_DOWNLOAD_DIRECTORY_MODE })\n /* v8 ignore next -- best-effort chmod */\n await chmod(stagingDirectory, PRIVATE_DOWNLOAD_DIRECTORY_MODE).catch(() => {})\n try {\n const realStagingDirectory = await realpath(stagingDirectory)\n assertPathInsideRoot(realManagedDirectory, realStagingDirectory, path)\n await assertDownloadPathSameDevice(\n rootRealPath,\n realStagingDirectory,\n statForDeviceCheck,\n 'unsafe local destination path: cannot stage download across filesystems',\n )\n await beforeStagingMarkerWrite?.(realStagingDirectory)\n await writeStagingMarker(realStagingDirectory, path)\n return realStagingDirectory\n } catch (err) {\n /* v8 ignore next -- best-effort cleanup */\n await rm(stagingDirectory, { recursive: true, force: true }).catch(() => {})\n throw err\n }\n}\n\n/**\n * Returns true when a scan-root entry is an SDK-managed download staging root.\n * @param directory - Candidate directory to inspect.\n *\n * @returns True when the directory contains SDK staging markers.\n *\n * @internal\n */\nexport async function isManagedDownloadStagingRoot(directory: string): Promise<boolean> {\n const { lstat, readdir } = await import('node:fs/promises')\n const path = await import('node:path')\n\n try {\n const markerStats = await lstat(path.join(directory, DOWNLOAD_STAGING_MARKER_NAME))\n if (markerStats.isFile()) return true\n } catch (err) {\n if (!hasErrorCode(err, 'ENOENT')) return false\n }\n\n let entries: import('node:fs').Dirent[]\n try {\n entries = await readdir(directory, { withFileTypes: true })\n } catch {\n return false\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith(DOWNLOAD_STAGING_ENTRY_SUFFIX)) continue\n try {\n const markerStats = await lstat(\n path.join(directory, entry.name, DOWNLOAD_STAGING_MARKER_NAME),\n )\n if (markerStats.isFile()) return true\n } catch {\n // Keep checking siblings; one corrupt entry should not hide user content.\n }\n }\n return false\n}\n\nconst PRIVATE_DOWNLOAD_FILE_MODE = 0o600\nconst PRIVATE_DOWNLOAD_DIRECTORY_MODE = 0o700\n\nasync function writeStagingMarker(\n directory: string,\n path: typeof import('node:path'),\n): Promise<void> {\n const { constants } = await import('node:fs')\n const { lstat, open } = await import('node:fs/promises')\n const markerPath = path.join(directory, DOWNLOAD_STAGING_MARKER_NAME)\n let handle: Awaited<ReturnType<typeof open>> | undefined\n try {\n handle = await open(\n markerPath,\n constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(constants),\n PRIVATE_DOWNLOAD_FILE_MODE,\n )\n /* v8 ignore next -- best-effort chmod */\n await handle.chmod(PRIVATE_DOWNLOAD_FILE_MODE).catch(() => {})\n } catch (err) {\n if (hasErrorCode(err, 'EEXIST') || hasErrorCode(err, 'ELOOP')) {\n const markerStats = await lstat(markerPath).catch(() => undefined)\n if (markerStats?.isFile() === true) return\n throw new Error('unsafe local destination path: staging marker is not a regular file')\n }\n throw err\n } finally {\n /* v8 ignore next -- best-effort close */\n await handle?.close().catch(() => {})\n }\n}\n\n/**\n * Verifies that a candidate path is on the same filesystem device as the root.\n * @param rootRealPath - Resolved local sync root path.\n * @param candidateRealPath - Resolved candidate path to compare.\n * @param statForDeviceCheck - Stat function used to read device IDs.\n * @param message - Error message used when devices differ.\n *\n * @internal\n */\nexport async function assertDownloadPathSameDevice(\n rootRealPath: string,\n candidateRealPath: string,\n statForDeviceCheck: DeviceStatFn,\n message: string,\n): Promise<void> {\n const [rootStats, candidateStats] = await Promise.all([\n statForDeviceCheck(rootRealPath),\n statForDeviceCheck(candidateRealPath),\n ])\n if (rootStats.dev !== candidateStats.dev) throw new Error(message)\n}\n\nasync function reapStaleDownloadStagingDirectoriesOnce(\n managedDirectory: string,\n path: typeof import('node:path'),\n nowMillis: number,\n): Promise<void> {\n const previous = reapedManagedDirectories.get(managedDirectory)\n if (previous !== undefined) {\n await previous\n return\n }\n\n const next = reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis).finally(\n () => {\n if (reapedManagedDirectories.get(managedDirectory) === next) {\n reapedManagedDirectories.delete(managedDirectory)\n }\n },\n )\n reapedManagedDirectories.set(managedDirectory, next)\n await next\n}\n\nasync function reapStaleDownloadStagingDirectories(\n managedDirectory: string,\n path: typeof import('node:path'),\n nowMillis: number,\n): Promise<void> {\n const { readdir, realpath, rm } = await import('node:fs/promises')\n let entries: import('node:fs').Dirent[]\n try {\n entries = await readdir(managedDirectory, { withFileTypes: true })\n } catch (err) {\n if (hasErrorCode(err, 'ENOENT')) return\n emitCleanupWarning('failed to inspect B2 SDK download staging entries')\n return\n }\n\n const cleanupErrors: CleanupWarning[] = []\n await forEachWithConcurrency(entries, MAX_STAGING_CLEANUP_CONCURRENCY, async (entry) => {\n if (!entry.isDirectory() || !entry.name.endsWith(DOWNLOAD_STAGING_ENTRY_SUFFIX)) return\n const candidate = path.join(managedDirectory, entry.name)\n const activity = await readManagedStagingEntryActivity(candidate, path)\n if (activity === undefined || !stagingActivityIsStale(activity, nowMillis)) return\n const realCandidate = await realpath(candidate).catch(() => {\n cleanupErrors.push({ entryName: entry.name, operation: 'inspect' })\n return undefined\n })\n if (realCandidate === undefined) return\n try {\n assertPathInsideRoot(managedDirectory, realCandidate, path)\n const latestActivity = await readManagedStagingEntryActivity(candidate, path)\n if (\n latestActivity === undefined ||\n latestActivity.signature !== activity.signature ||\n !stagingActivityIsStale(latestActivity, nowMillis)\n ) {\n return\n }\n await rm(realCandidate, { recursive: true, force: true })\n } catch {\n cleanupErrors.push({ entryName: entry.name, operation: 'remove' })\n }\n })\n\n if (cleanupErrors.length > 0) {\n const noun = cleanupErrors.length === 1 ? 'entry' : 'entries'\n emitCleanupWarning(\n `failed to reap ${cleanupErrors.length} stale B2 SDK download staging ${noun}: ${cleanupErrors\n .slice(0, MAX_CLEANUP_WARNING_ENTRIES)\n .map(formatCleanupWarning)\n .join('; ')}`,\n )\n }\n}\n\nasync function readManagedStagingEntryActivity(\n candidate: string,\n path: typeof import('node:path'),\n): Promise<StagingActivitySnapshot | undefined> {\n const { lstat, readdir } = await import('node:fs/promises')\n\n try {\n const [directoryStats, markerStats] = await Promise.all([\n lstat(candidate),\n lstat(path.join(candidate, DOWNLOAD_STAGING_MARKER_NAME)),\n ])\n if (!directoryStats.isDirectory() || !markerStats.isFile()) return undefined\n\n const entries = await readdir(candidate, { withFileTypes: true })\n if (entries.length > DOWNLOAD_STAGING_ACTIVITY_ENTRY_LIMIT) return undefined\n\n const statsParts = [\n `.:${stagingStatsSignature(directoryStats)}`,\n `${DOWNLOAD_STAGING_MARKER_NAME}:${stagingStatsSignature(markerStats)}`,\n ]\n let newestActivityMs = Math.max(\n stagingStatsActivityMs(directoryStats),\n stagingStatsActivityMs(markerStats),\n )\n\n for (const entry of [...entries].sort((a, b) =>\n a.name < b.name ? -1 : a.name > b.name ? 1 : 0,\n )) {\n const stats = await lstat(path.join(candidate, entry.name))\n statsParts.push(`${entry.name}:${stagingStatsSignature(stats)}`)\n newestActivityMs = Math.max(newestActivityMs, stagingStatsActivityMs(stats))\n }\n\n return { newestActivityMs, signature: statsParts.join('|') }\n } catch {\n return undefined\n }\n}\n\nfunction stagingStatsActivityMs(stats: { readonly mtimeMs: number }) {\n return stats.mtimeMs\n}\n\nfunction stagingStatsSignature(stats: {\n readonly ctimeMs: number\n readonly dev: number | bigint\n readonly ino: number | bigint\n readonly mtimeMs: number\n readonly size: number\n}): string {\n return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`\n}\n\nfunction stagingActivityIsStale(activity: StagingActivitySnapshot, nowMillis: number): boolean {\n return nowMillis - activity.newestActivityMs >= STALE_DOWNLOAD_STAGING_AGE_MS\n}\n\nfunction formatCleanupWarning(error: CleanupWarning): string {\n return `${error.operation} ${safeWarningName(error.entryName)}`\n}\n\nfunction safeWarningName(name: string): string {\n const chars = Array.from(name)\n const bounded = chars.slice(0, 80).join('')\n const suffix = chars.length > 80 ? '...' : ''\n return JSON.stringify(`${bounded.replace(/[^A-Za-z0-9._-]/g, '?')}${suffix}`)\n}\n\nfunction canonicalLocalFilesystemSegment(segment: string): string {\n return segment.normalize('NFC').toLocaleLowerCase('en-US')\n}\n\nasync function forEachWithConcurrency<T>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T) => Promise<void>,\n): Promise<void> {\n let index = 0\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (index < items.length) {\n const item = items[index]\n index += 1\n if (item !== undefined) await fn(item)\n }\n })\n await Promise.all(workers)\n}\n\nfunction emitCleanupWarning(message: string): void {\n const processLike = (\n globalThis as {\n process?: {\n emitWarning?: (warning: string, options?: { readonly code?: string }) => void\n }\n }\n ).process\n processLike?.emitWarning?.(message, { code: 'B2SDK_DOWNLOAD_STAGING_CLEANUP_FAILED' })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"download-staging.cjs","names":[],"sources":["../../src/sync/download-staging.ts"],"sourcesContent":["import { assertPathInsideRoot, hasErrorCode, noFollowFlag } from './path-safety.ts'\n\ntype DeviceStat = { readonly dev: number | bigint }\ntype DeviceStatFn = (path: string) => Promise<DeviceStat>\n\n/** @internal */\nexport const DOWNLOAD_STAGING_DIRECTORY_NAME = '.b2sdk-download-staging'\n/** @internal */\nexport const DOWNLOAD_STAGING_MARKER_NAME = '.b2sdk-staging-marker.partial'\n\nconst CANONICAL_DOWNLOAD_STAGING_DIRECTORY_NAME = canonicalLocalFilesystemSegment(\n DOWNLOAD_STAGING_DIRECTORY_NAME,\n)\nconst DOWNLOAD_STAGING_ENTRY_SUFFIX = '.download'\nconst STALE_DOWNLOAD_STAGING_AGE_MS = 24 * 60 * 60 * 1000\nconst MAX_STAGING_CLEANUP_CONCURRENCY = 8\nconst MAX_CLEANUP_WARNING_ENTRIES = 3\n/** @internal */\nexport const DOWNLOAD_STAGING_ACTIVITY_ENTRY_LIMIT = 1024\nconst reapedManagedDirectories = new Map<string, Promise<void>>()\n\ntype StagingActivitySnapshot = {\n readonly newestActivityMs: number\n readonly signature: string\n}\n\ntype CleanupWarning = {\n readonly entryName: string\n readonly operation: 'inspect' | 'remove'\n}\n\n/**\n * Checks whether a single path segment matches the SDK-managed staging directory name.\n * @param segment - Candidate path segment.\n *\n * @returns True when the segment is the reserved staging directory name under\n * local filesystem canonicalization.\n *\n * @internal\n */\nexport function isDownloadStagingDirectorySegment(segment: string | undefined): boolean {\n return (\n segment !== undefined &&\n canonicalLocalFilesystemSegment(segment) === CANONICAL_DOWNLOAD_STAGING_DIRECTORY_NAME\n )\n}\n\n/**\n * Creates a private SDK-managed staging directory under a local sync root.\n * @param rootRealPath - Resolved local sync root path.\n * @param path - Node path module used for platform-specific path operations.\n * @param randomUUID - UUID provider used to create unique staging entries.\n * @param statForDeviceCheck - Stat function used to verify filesystem devices.\n * @param beforeStagingMarkerWrite - Test hook called before marker creation.\n * @param activityEntryLimit - Maximum entry count inspected for stale staging activity.\n *\n * @returns The resolved staging directory path.\n *\n * @internal\n */\nexport async function createDownloadStagingDirectory(\n rootRealPath: string,\n path: typeof import('node:path'),\n randomUUID: () => string,\n statForDeviceCheck: DeviceStatFn,\n beforeStagingMarkerWrite?: (directory: string) => Promise<void> | void,\n activityEntryLimit = DOWNLOAD_STAGING_ACTIVITY_ENTRY_LIMIT,\n): Promise<string> {\n const { chmod, lstat, mkdir, readdir, realpath, rm } = await import('node:fs/promises')\n const managedDirectory = path.join(rootRealPath, DOWNLOAD_STAGING_DIRECTORY_NAME)\n try {\n await mkdir(managedDirectory, { mode: PRIVATE_DOWNLOAD_DIRECTORY_MODE })\n } catch (err) {\n if (!hasErrorCode(err, 'EEXIST')) throw err\n }\n const managedStats = await lstat(managedDirectory)\n if (!managedStats.isDirectory()) {\n throw new Error(\n `unsafe local destination path: ${DOWNLOAD_STAGING_DIRECTORY_NAME} is not a directory`,\n )\n }\n const realManagedDirectory = await realpath(managedDirectory)\n assertPathInsideRoot(rootRealPath, realManagedDirectory, path)\n await assertDownloadPathSameDevice(\n rootRealPath,\n realManagedDirectory,\n statForDeviceCheck,\n 'unsafe local destination path: cannot stage download across filesystems',\n )\n\n if (!(await isManagedDownloadStagingRoot(realManagedDirectory))) {\n const entries = await readdir(realManagedDirectory)\n if (entries.length > 0 && !(await isManagedDownloadStagingRoot(realManagedDirectory))) {\n throw new Error(\n `unsafe local destination path: ${DOWNLOAD_STAGING_DIRECTORY_NAME} is reserved for SDK download staging`,\n )\n }\n }\n await beforeStagingMarkerWrite?.(realManagedDirectory)\n await writeStagingMarker(realManagedDirectory, path)\n /* v8 ignore next -- best-effort chmod */\n await chmod(realManagedDirectory, PRIVATE_DOWNLOAD_DIRECTORY_MODE).catch(() => {})\n await reapStaleDownloadStagingDirectoriesOnce(\n realManagedDirectory,\n path,\n Date.now(),\n activityEntryLimit,\n )\n\n const stagingDirectory = path.join(\n realManagedDirectory,\n `${Date.now()}-${randomUUID()}${DOWNLOAD_STAGING_ENTRY_SUFFIX}`,\n )\n await mkdir(stagingDirectory, { mode: PRIVATE_DOWNLOAD_DIRECTORY_MODE })\n /* v8 ignore next -- best-effort chmod */\n await chmod(stagingDirectory, PRIVATE_DOWNLOAD_DIRECTORY_MODE).catch(() => {})\n try {\n const realStagingDirectory = await realpath(stagingDirectory)\n assertPathInsideRoot(realManagedDirectory, realStagingDirectory, path)\n await assertDownloadPathSameDevice(\n rootRealPath,\n realStagingDirectory,\n statForDeviceCheck,\n 'unsafe local destination path: cannot stage download across filesystems',\n )\n await beforeStagingMarkerWrite?.(realStagingDirectory)\n await writeStagingMarker(realStagingDirectory, path)\n return realStagingDirectory\n } catch (err) {\n /* v8 ignore next -- best-effort cleanup */\n await rm(stagingDirectory, { recursive: true, force: true }).catch(() => {})\n throw err\n }\n}\n\n/**\n * Returns true when a scan-root entry is an SDK-managed download staging root.\n * @param directory - Candidate directory to inspect.\n *\n * @returns True when the directory contains SDK staging markers.\n *\n * @internal\n */\nexport async function isManagedDownloadStagingRoot(directory: string): Promise<boolean> {\n const { lstat, readdir } = await import('node:fs/promises')\n const path = await import('node:path')\n\n try {\n const markerStats = await lstat(path.join(directory, DOWNLOAD_STAGING_MARKER_NAME))\n if (markerStats.isFile()) return true\n } catch (err) {\n if (!hasErrorCode(err, 'ENOENT')) return false\n }\n\n let entries: import('node:fs').Dirent[]\n try {\n entries = await readdir(directory, { withFileTypes: true })\n } catch {\n return false\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory() || !entry.name.endsWith(DOWNLOAD_STAGING_ENTRY_SUFFIX)) continue\n try {\n const markerStats = await lstat(\n path.join(directory, entry.name, DOWNLOAD_STAGING_MARKER_NAME),\n )\n if (markerStats.isFile()) return true\n } catch {\n // Keep checking siblings; one corrupt entry should not hide user content.\n }\n }\n return false\n}\n\nconst PRIVATE_DOWNLOAD_FILE_MODE = 0o600\nconst PRIVATE_DOWNLOAD_DIRECTORY_MODE = 0o700\n\nasync function writeStagingMarker(\n directory: string,\n path: typeof import('node:path'),\n): Promise<void> {\n const { constants } = await import('node:fs')\n const { lstat, open } = await import('node:fs/promises')\n const markerPath = path.join(directory, DOWNLOAD_STAGING_MARKER_NAME)\n let handle: Awaited<ReturnType<typeof open>> | undefined\n try {\n handle = await open(\n markerPath,\n constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(constants),\n PRIVATE_DOWNLOAD_FILE_MODE,\n )\n /* v8 ignore next -- best-effort chmod */\n await handle.chmod(PRIVATE_DOWNLOAD_FILE_MODE).catch(() => {})\n } catch (err) {\n if (hasErrorCode(err, 'EEXIST') || hasErrorCode(err, 'ELOOP')) {\n const markerStats = await lstat(markerPath).catch(() => undefined)\n if (markerStats?.isFile() === true) return\n throw new Error('unsafe local destination path: staging marker is not a regular file')\n }\n throw err\n } finally {\n /* v8 ignore next -- best-effort close */\n await handle?.close().catch(() => {})\n }\n}\n\n/**\n * Verifies that a candidate path is on the same filesystem device as the root.\n * @param rootRealPath - Resolved local sync root path.\n * @param candidateRealPath - Resolved candidate path to compare.\n * @param statForDeviceCheck - Stat function used to read device IDs.\n * @param message - Error message used when devices differ.\n *\n * @internal\n */\nexport async function assertDownloadPathSameDevice(\n rootRealPath: string,\n candidateRealPath: string,\n statForDeviceCheck: DeviceStatFn,\n message: string,\n): Promise<void> {\n const [rootStats, candidateStats] = await Promise.all([\n statForDeviceCheck(rootRealPath),\n statForDeviceCheck(candidateRealPath),\n ])\n if (rootStats.dev !== candidateStats.dev) throw new Error(message)\n}\n\nasync function reapStaleDownloadStagingDirectoriesOnce(\n managedDirectory: string,\n path: typeof import('node:path'),\n nowMillis: number,\n activityEntryLimit: number,\n): Promise<void> {\n const previous = reapedManagedDirectories.get(managedDirectory)\n if (previous !== undefined) {\n await previous\n return\n }\n\n const next = reapStaleDownloadStagingDirectories(\n managedDirectory,\n path,\n nowMillis,\n activityEntryLimit,\n ).finally(() => {\n if (reapedManagedDirectories.get(managedDirectory) === next) {\n reapedManagedDirectories.delete(managedDirectory)\n }\n })\n reapedManagedDirectories.set(managedDirectory, next)\n await next\n}\n\nasync function reapStaleDownloadStagingDirectories(\n managedDirectory: string,\n path: typeof import('node:path'),\n nowMillis: number,\n activityEntryLimit: number,\n): Promise<void> {\n const { readdir, realpath, rm } = await import('node:fs/promises')\n let entries: import('node:fs').Dirent[]\n try {\n entries = await readdir(managedDirectory, { withFileTypes: true })\n } catch (err) {\n if (hasErrorCode(err, 'ENOENT')) return\n emitCleanupWarning('failed to inspect B2 SDK download staging entries')\n return\n }\n\n const cleanupErrors: CleanupWarning[] = []\n await forEachWithConcurrency(entries, MAX_STAGING_CLEANUP_CONCURRENCY, async (entry) => {\n if (!entry.isDirectory() || !entry.name.endsWith(DOWNLOAD_STAGING_ENTRY_SUFFIX)) return\n const candidate = path.join(managedDirectory, entry.name)\n const activity = await readManagedStagingEntryActivity(candidate, path, activityEntryLimit)\n if (activity === undefined || !stagingActivityIsStale(activity, nowMillis)) return\n const realCandidate = await realpath(candidate).catch(() => {\n cleanupErrors.push({ entryName: entry.name, operation: 'inspect' })\n return undefined\n })\n if (realCandidate === undefined) return\n try {\n assertPathInsideRoot(managedDirectory, realCandidate, path)\n const latestActivity = await readManagedStagingEntryActivity(\n candidate,\n path,\n activityEntryLimit,\n )\n if (\n latestActivity === undefined ||\n latestActivity.signature !== activity.signature ||\n !stagingActivityIsStale(latestActivity, nowMillis)\n ) {\n return\n }\n await rm(realCandidate, { recursive: true, force: true })\n } catch {\n cleanupErrors.push({ entryName: entry.name, operation: 'remove' })\n }\n })\n\n if (cleanupErrors.length > 0) {\n const noun = cleanupErrors.length === 1 ? 'entry' : 'entries'\n emitCleanupWarning(\n `failed to reap ${cleanupErrors.length} stale B2 SDK download staging ${noun}: ${cleanupErrors\n .slice(0, MAX_CLEANUP_WARNING_ENTRIES)\n .map(formatCleanupWarning)\n .join('; ')}`,\n )\n }\n}\n\nasync function readManagedStagingEntryActivity(\n candidate: string,\n path: typeof import('node:path'),\n activityEntryLimit: number,\n): Promise<StagingActivitySnapshot | undefined> {\n const { lstat, readdir } = await import('node:fs/promises')\n\n try {\n const [directoryStats, markerStats] = await Promise.all([\n lstat(candidate),\n lstat(path.join(candidate, DOWNLOAD_STAGING_MARKER_NAME)),\n ])\n if (!directoryStats.isDirectory() || !markerStats.isFile()) return undefined\n\n const entries = await readdir(candidate, { withFileTypes: true })\n if (entries.length > activityEntryLimit) return undefined\n\n const statsParts = [\n `.:${stagingStatsSignature(directoryStats)}`,\n `${DOWNLOAD_STAGING_MARKER_NAME}:${stagingStatsSignature(markerStats)}`,\n ]\n let newestActivityMs = Math.max(\n stagingStatsActivityMs(directoryStats),\n stagingStatsActivityMs(markerStats),\n )\n\n for (const entry of [...entries].sort((a, b) =>\n a.name < b.name ? -1 : a.name > b.name ? 1 : 0,\n )) {\n const stats = await lstat(path.join(candidate, entry.name))\n statsParts.push(`${entry.name}:${stagingStatsSignature(stats)}`)\n newestActivityMs = Math.max(newestActivityMs, stagingStatsActivityMs(stats))\n }\n\n return { newestActivityMs, signature: statsParts.join('|') }\n } catch {\n return undefined\n }\n}\n\nfunction stagingStatsActivityMs(stats: { readonly mtimeMs: number }) {\n return stats.mtimeMs\n}\n\nfunction stagingStatsSignature(stats: {\n readonly ctimeMs: number\n readonly dev: number | bigint\n readonly ino: number | bigint\n readonly mtimeMs: number\n readonly size: number\n}): string {\n return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeMs}:${stats.ctimeMs}`\n}\n\nfunction stagingActivityIsStale(activity: StagingActivitySnapshot, nowMillis: number): boolean {\n return nowMillis - activity.newestActivityMs >= STALE_DOWNLOAD_STAGING_AGE_MS\n}\n\nfunction formatCleanupWarning(error: CleanupWarning): string {\n return `${error.operation} ${safeWarningName(error.entryName)}`\n}\n\nfunction safeWarningName(name: string): string {\n const chars = Array.from(name)\n const bounded = chars.slice(0, 80).join('')\n const suffix = chars.length > 80 ? '...' : ''\n return JSON.stringify(`${bounded.replace(/[^A-Za-z0-9._-]/g, '?')}${suffix}`)\n}\n\nfunction canonicalLocalFilesystemSegment(segment: string): string {\n return segment.normalize('NFC').toLocaleLowerCase('en-US')\n}\n\nasync function forEachWithConcurrency<T>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T) => Promise<void>,\n): Promise<void> {\n let index = 0\n const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {\n while (index < items.length) {\n const item = items[index]\n index += 1\n if (item !== undefined) await fn(item)\n }\n })\n await Promise.all(workers)\n}\n\nfunction emitCleanupWarning(message: string): void {\n const processLike = (\n globalThis as {\n process?: {\n emitWarning?: (warning: string, options?: { readonly code?: string }) => void\n }\n }\n ).process\n processLike?.emitWarning?.(message, { code: 'B2SDK_DOWNLOAD_STAGING_CLEANUP_FAILED' })\n}\n"],"mappings":"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"}
|
|
@@ -25,12 +25,13 @@ export declare function isDownloadStagingDirectorySegment(segment: string | unde
|
|
|
25
25
|
* @param randomUUID - UUID provider used to create unique staging entries.
|
|
26
26
|
* @param statForDeviceCheck - Stat function used to verify filesystem devices.
|
|
27
27
|
* @param beforeStagingMarkerWrite - Test hook called before marker creation.
|
|
28
|
+
* @param activityEntryLimit - Maximum entry count inspected for stale staging activity.
|
|
28
29
|
*
|
|
29
30
|
* @returns The resolved staging directory path.
|
|
30
31
|
*
|
|
31
32
|
* @internal
|
|
32
33
|
*/
|
|
33
|
-
export declare function createDownloadStagingDirectory(rootRealPath: string, path: typeof import('node:path'), randomUUID: () => string, statForDeviceCheck: DeviceStatFn, beforeStagingMarkerWrite?: (directory: string) => Promise<void> | void): Promise<string>;
|
|
34
|
+
export declare function createDownloadStagingDirectory(rootRealPath: string, path: typeof import('node:path'), randomUUID: () => string, statForDeviceCheck: DeviceStatFn, beforeStagingMarkerWrite?: (directory: string) => Promise<void> | void, activityEntryLimit?: number): Promise<string>;
|
|
34
35
|
/**
|
|
35
36
|
* Returns true when a scan-root entry is an SDK-managed download staging root.
|
|
36
37
|
* @param directory - Candidate directory to inspect.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download-staging.d.ts","sourceRoot":"","sources":["../../src/sync/download-staging.js"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AACnD,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;AAEzD,gBAAgB;AAChB,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AACxE,gBAAgB;AAChB,eAAO,MAAM,4BAA4B,kCAAkC,CAAA;AAS3E,gBAAgB;AAChB,eAAO,MAAM,qCAAqC,OAAO,CAAA;AAazD;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKtF;AAED
|
|
1
|
+
{"version":3,"file":"download-staging.d.ts","sourceRoot":"","sources":["../../src/sync/download-staging.js"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AACnD,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;AAEzD,gBAAgB;AAChB,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AACxE,gBAAgB;AAChB,eAAO,MAAM,4BAA4B,kCAAkC,CAAA;AAS3E,gBAAgB;AAChB,eAAO,MAAM,qCAAqC,OAAO,CAAA;AAazD;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKtF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,8BAA8B,CAClD,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,cAAc,WAAW,CAAC,EAChC,UAAU,EAAE,MAAM,MAAM,EACxB,kBAAkB,EAAE,YAAY,EAChC,wBAAwB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EACtE,kBAAkB,SAAwC,GACzD,OAAO,CAAC,MAAM,CAAC,CAkEjB;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BtF;AAkCD;;;;;;;;GAQG;AACH,wBAAsB,4BAA4B,CAChD,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,EACzB,kBAAkB,EAAE,YAAY,EAChC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -25,12 +25,13 @@ export declare function isDownloadStagingDirectorySegment(segment: string | unde
|
|
|
25
25
|
* @param randomUUID - UUID provider used to create unique staging entries.
|
|
26
26
|
* @param statForDeviceCheck - Stat function used to verify filesystem devices.
|
|
27
27
|
* @param beforeStagingMarkerWrite - Test hook called before marker creation.
|
|
28
|
+
* @param activityEntryLimit - Maximum entry count inspected for stale staging activity.
|
|
28
29
|
*
|
|
29
30
|
* @returns The resolved staging directory path.
|
|
30
31
|
*
|
|
31
32
|
* @internal
|
|
32
33
|
*/
|
|
33
|
-
export declare function createDownloadStagingDirectory(rootRealPath: string, path: typeof import('node:path'), randomUUID: () => string, statForDeviceCheck: DeviceStatFn, beforeStagingMarkerWrite?: (directory: string) => Promise<void> | void): Promise<string>;
|
|
34
|
+
export declare function createDownloadStagingDirectory(rootRealPath: string, path: typeof import('node:path'), randomUUID: () => string, statForDeviceCheck: DeviceStatFn, beforeStagingMarkerWrite?: (directory: string) => Promise<void> | void, activityEntryLimit?: number): Promise<string>;
|
|
34
35
|
/**
|
|
35
36
|
* Returns true when a scan-root entry is an SDK-managed download staging root.
|
|
36
37
|
* @param directory - Candidate directory to inspect.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download-staging.d.ts","sourceRoot":"","sources":["../../src/sync/download-staging.js"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AACnD,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;AAEzD,gBAAgB;AAChB,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AACxE,gBAAgB;AAChB,eAAO,MAAM,4BAA4B,kCAAkC,CAAA;AAS3E,gBAAgB;AAChB,eAAO,MAAM,qCAAqC,OAAO,CAAA;AAazD;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKtF;AAED
|
|
1
|
+
{"version":3,"file":"download-staging.d.ts","sourceRoot":"","sources":["../../src/sync/download-staging.js"],"names":[],"mappings":"AAEA,KAAK,UAAU,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,CAAA;AACnD,KAAK,YAAY,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;AAEzD,gBAAgB;AAChB,eAAO,MAAM,+BAA+B,4BAA4B,CAAA;AACxE,gBAAgB;AAChB,eAAO,MAAM,4BAA4B,kCAAkC,CAAA;AAS3E,gBAAgB;AAChB,eAAO,MAAM,qCAAqC,OAAO,CAAA;AAazD;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKtF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,8BAA8B,CAClD,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,cAAc,WAAW,CAAC,EAChC,UAAU,EAAE,MAAM,MAAM,EACxB,kBAAkB,EAAE,YAAY,EAChC,wBAAwB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,EACtE,kBAAkB,SAAwC,GACzD,OAAO,CAAC,MAAM,CAAC,CAkEjB;AAED;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8BtF;AAkCD;;;;;;;;GAQG;AACH,wBAAsB,4BAA4B,CAChD,YAAY,EAAE,MAAM,EACpB,iBAAiB,EAAE,MAAM,EACzB,kBAAkB,EAAE,YAAY,EAChC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -6,7 +6,7 @@ var DOWNLOAD_STAGING_DIRECTORY_NAME = ".b2sdk-download-staging";
|
|
|
6
6
|
var DOWNLOAD_STAGING_MARKER_NAME = ".b2sdk-staging-marker.partial";
|
|
7
7
|
var CANONICAL_DOWNLOAD_STAGING_DIRECTORY_NAME = canonicalLocalFilesystemSegment(DOWNLOAD_STAGING_DIRECTORY_NAME);
|
|
8
8
|
var DOWNLOAD_STAGING_ENTRY_SUFFIX = ".download";
|
|
9
|
-
var STALE_DOWNLOAD_STAGING_AGE_MS =
|
|
9
|
+
var STALE_DOWNLOAD_STAGING_AGE_MS = 864e5;
|
|
10
10
|
var MAX_STAGING_CLEANUP_CONCURRENCY = 8;
|
|
11
11
|
var MAX_CLEANUP_WARNING_ENTRIES = 3;
|
|
12
12
|
/** @internal */
|
|
@@ -31,12 +31,13 @@ function isDownloadStagingDirectorySegment(segment) {
|
|
|
31
31
|
* @param randomUUID - UUID provider used to create unique staging entries.
|
|
32
32
|
* @param statForDeviceCheck - Stat function used to verify filesystem devices.
|
|
33
33
|
* @param beforeStagingMarkerWrite - Test hook called before marker creation.
|
|
34
|
+
* @param activityEntryLimit - Maximum entry count inspected for stale staging activity.
|
|
34
35
|
*
|
|
35
36
|
* @returns The resolved staging directory path.
|
|
36
37
|
*
|
|
37
38
|
* @internal
|
|
38
39
|
*/
|
|
39
|
-
async function createDownloadStagingDirectory(rootRealPath, path, randomUUID, statForDeviceCheck, beforeStagingMarkerWrite) {
|
|
40
|
+
async function createDownloadStagingDirectory(rootRealPath, path, randomUUID, statForDeviceCheck, beforeStagingMarkerWrite, activityEntryLimit = DOWNLOAD_STAGING_ACTIVITY_ENTRY_LIMIT) {
|
|
40
41
|
const { chmod, lstat, mkdir, readdir, realpath, rm } = await import("node:fs/promises");
|
|
41
42
|
const managedDirectory = path.join(rootRealPath, DOWNLOAD_STAGING_DIRECTORY_NAME);
|
|
42
43
|
try {
|
|
@@ -55,7 +56,7 @@ async function createDownloadStagingDirectory(rootRealPath, path, randomUUID, st
|
|
|
55
56
|
await writeStagingMarker(realManagedDirectory, path);
|
|
56
57
|
/* v8 ignore next -- best-effort chmod */
|
|
57
58
|
await chmod(realManagedDirectory, PRIVATE_DOWNLOAD_DIRECTORY_MODE).catch(() => {});
|
|
58
|
-
await reapStaleDownloadStagingDirectoriesOnce(realManagedDirectory, path, Date.now());
|
|
59
|
+
await reapStaleDownloadStagingDirectoriesOnce(realManagedDirectory, path, Date.now(), activityEntryLimit);
|
|
59
60
|
const stagingDirectory = path.join(realManagedDirectory, `${Date.now()}-${randomUUID()}${DOWNLOAD_STAGING_ENTRY_SUFFIX}`);
|
|
60
61
|
await mkdir(stagingDirectory, { mode: PRIVATE_DOWNLOAD_DIRECTORY_MODE });
|
|
61
62
|
/* v8 ignore next -- best-effort chmod */
|
|
@@ -141,19 +142,19 @@ async function assertDownloadPathSameDevice(rootRealPath, candidateRealPath, sta
|
|
|
141
142
|
const [rootStats, candidateStats] = await Promise.all([statForDeviceCheck(rootRealPath), statForDeviceCheck(candidateRealPath)]);
|
|
142
143
|
if (rootStats.dev !== candidateStats.dev) throw new Error(message);
|
|
143
144
|
}
|
|
144
|
-
async function reapStaleDownloadStagingDirectoriesOnce(managedDirectory, path, nowMillis) {
|
|
145
|
+
async function reapStaleDownloadStagingDirectoriesOnce(managedDirectory, path, nowMillis, activityEntryLimit) {
|
|
145
146
|
const previous = reapedManagedDirectories.get(managedDirectory);
|
|
146
147
|
if (previous !== void 0) {
|
|
147
148
|
await previous;
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
|
-
const next = reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis).finally(() => {
|
|
151
|
+
const next = reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis, activityEntryLimit).finally(() => {
|
|
151
152
|
if (reapedManagedDirectories.get(managedDirectory) === next) reapedManagedDirectories.delete(managedDirectory);
|
|
152
153
|
});
|
|
153
154
|
reapedManagedDirectories.set(managedDirectory, next);
|
|
154
155
|
await next;
|
|
155
156
|
}
|
|
156
|
-
async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis) {
|
|
157
|
+
async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMillis, activityEntryLimit) {
|
|
157
158
|
const { readdir, realpath, rm } = await import("node:fs/promises");
|
|
158
159
|
let entries;
|
|
159
160
|
try {
|
|
@@ -167,7 +168,7 @@ async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMi
|
|
|
167
168
|
await forEachWithConcurrency(entries, MAX_STAGING_CLEANUP_CONCURRENCY, async (entry) => {
|
|
168
169
|
if (!entry.isDirectory() || !entry.name.endsWith(DOWNLOAD_STAGING_ENTRY_SUFFIX)) return;
|
|
169
170
|
const candidate = path.join(managedDirectory, entry.name);
|
|
170
|
-
const activity = await readManagedStagingEntryActivity(candidate, path);
|
|
171
|
+
const activity = await readManagedStagingEntryActivity(candidate, path, activityEntryLimit);
|
|
171
172
|
if (activity === void 0 || !stagingActivityIsStale(activity, nowMillis)) return;
|
|
172
173
|
const realCandidate = await realpath(candidate).catch(() => {
|
|
173
174
|
cleanupErrors.push({
|
|
@@ -178,7 +179,7 @@ async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMi
|
|
|
178
179
|
if (realCandidate === void 0) return;
|
|
179
180
|
try {
|
|
180
181
|
assertPathInsideRoot(managedDirectory, realCandidate, path);
|
|
181
|
-
const latestActivity = await readManagedStagingEntryActivity(candidate, path);
|
|
182
|
+
const latestActivity = await readManagedStagingEntryActivity(candidate, path, activityEntryLimit);
|
|
182
183
|
if (latestActivity === void 0 || latestActivity.signature !== activity.signature || !stagingActivityIsStale(latestActivity, nowMillis)) return;
|
|
183
184
|
await rm(realCandidate, {
|
|
184
185
|
recursive: true,
|
|
@@ -196,13 +197,13 @@ async function reapStaleDownloadStagingDirectories(managedDirectory, path, nowMi
|
|
|
196
197
|
emitCleanupWarning(`failed to reap ${cleanupErrors.length} stale B2 SDK download staging ${noun}: ${cleanupErrors.slice(0, MAX_CLEANUP_WARNING_ENTRIES).map(formatCleanupWarning).join("; ")}`);
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
|
-
async function readManagedStagingEntryActivity(candidate, path) {
|
|
200
|
+
async function readManagedStagingEntryActivity(candidate, path, activityEntryLimit) {
|
|
200
201
|
const { lstat, readdir } = await import("node:fs/promises");
|
|
201
202
|
try {
|
|
202
203
|
const [directoryStats, markerStats] = await Promise.all([lstat(candidate), lstat(path.join(candidate, DOWNLOAD_STAGING_MARKER_NAME))]);
|
|
203
204
|
if (!directoryStats.isDirectory() || !markerStats.isFile()) return void 0;
|
|
204
205
|
const entries = await readdir(candidate, { withFileTypes: true });
|
|
205
|
-
if (entries.length >
|
|
206
|
+
if (entries.length > activityEntryLimit) return void 0;
|
|
206
207
|
const statsParts = [`.:${stagingStatsSignature(directoryStats)}`, `${DOWNLOAD_STAGING_MARKER_NAME}:${stagingStatsSignature(markerStats)}`];
|
|
207
208
|
let newestActivityMs = Math.max(stagingStatsActivityMs(directoryStats), stagingStatsActivityMs(markerStats));
|
|
208
209
|
for (const entry of [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|