@kutur-kit/assets 0.0.0-stage → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +161 -2
- package/dist/Asset.d.ts +174 -0
- package/dist/Asset.d.ts.map +1 -0
- package/dist/Asset.js +156 -0
- package/dist/Asset.js.map +1 -0
- package/dist/AssetBucket.d.ts +73 -0
- package/dist/AssetBucket.d.ts.map +1 -0
- package/dist/AssetBucket.js +19 -0
- package/dist/AssetBucket.js.map +1 -0
- package/dist/AssetDelivery.d.ts +23 -0
- package/dist/AssetDelivery.d.ts.map +1 -0
- package/dist/AssetDelivery.js +12 -0
- package/dist/AssetDelivery.js.map +1 -0
- package/dist/AssetSigning.d.ts +109 -0
- package/dist/AssetSigning.d.ts.map +1 -0
- package/dist/AssetSigning.js +93 -0
- package/dist/AssetSigning.js.map +1 -0
- package/dist/alchemy/r2.d.ts +40 -0
- package/dist/alchemy/r2.d.ts.map +1 -0
- package/dist/alchemy/r2.js +83 -0
- package/dist/alchemy/r2.js.map +1 -0
- package/dist/delivery/local.d.ts +17 -0
- package/dist/delivery/local.d.ts.map +1 -0
- package/dist/delivery/local.js +20 -0
- package/dist/delivery/local.js.map +1 -0
- package/dist/filesystem.d.ts +15 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +90 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/bytes.d.ts +3 -0
- package/dist/internal/bytes.d.ts.map +1 -0
- package/dist/internal/bytes.js +11 -0
- package/dist/internal/bytes.js.map +1 -0
- package/dist/internal/http.d.ts +17 -0
- package/dist/internal/http.d.ts.map +1 -0
- package/dist/internal/http.js +38 -0
- package/dist/internal/http.js.map +1 -0
- package/dist/internal/sigv4.d.ts +15 -0
- package/dist/internal/sigv4.d.ts.map +1 -0
- package/dist/internal/sigv4.js +52 -0
- package/dist/internal/sigv4.js.map +1 -0
- package/dist/internal/url.d.ts +3 -0
- package/dist/internal/url.d.ts.map +1 -0
- package/dist/internal/url.js +3 -0
- package/dist/internal/url.js.map +1 -0
- package/dist/memory.d.ts +11 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +34 -0
- package/dist/memory.js.map +1 -0
- package/dist/s3.d.ts +42 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +67 -0
- package/dist/s3.js.map +1 -0
- package/dist/signing/local.d.ts +27 -0
- package/dist/signing/local.d.ts.map +1 -0
- package/dist/signing/local.js +128 -0
- package/dist/signing/local.js.map +1 -0
- package/dist/testing/AssetBucketContract.d.ts +18 -0
- package/dist/testing/AssetBucketContract.d.ts.map +1 -0
- package/dist/testing/AssetBucketContract.js +124 -0
- package/dist/testing/AssetBucketContract.js.map +1 -0
- package/dist/testing/AssetSigningContract.d.ts +18 -0
- package/dist/testing/AssetSigningContract.d.ts.map +1 -0
- package/dist/testing/AssetSigningContract.js +132 -0
- package/dist/testing/AssetSigningContract.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +109 -3
- package/src/Asset.ts +443 -0
- package/src/AssetBucket.ts +82 -0
- package/src/AssetDelivery.ts +41 -0
- package/src/AssetSigning.ts +206 -0
- package/src/alchemy/r2.ts +149 -0
- package/src/delivery/local.ts +41 -0
- package/src/filesystem.ts +166 -0
- package/src/index.ts +3 -0
- package/src/internal/bytes.ts +12 -0
- package/src/internal/http.ts +54 -0
- package/src/internal/sigv4.ts +84 -0
- package/src/internal/url.ts +2 -0
- package/src/memory.ts +65 -0
- package/src/s3.ts +138 -0
- package/src/signing/local.ts +210 -0
- package/src/testing/AssetBucketContract.ts +215 -0
- package/src/testing/AssetSigningContract.ts +254 -0
- package/src/testing/index.ts +3 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type * as Asset from "./Asset.ts";
|
|
2
|
+
import type * as AssetBucket from "./AssetBucket.ts";
|
|
3
|
+
import { Context, Effect, Layer } from "effect";
|
|
4
|
+
/** Public URLs for a bucket's objects, such as on a CDN. Only public buckets have them. */
|
|
5
|
+
export interface Delivery {
|
|
6
|
+
readonly url: (key: string) => string;
|
|
7
|
+
}
|
|
8
|
+
/** The service a bucket's `Delivery` is provided as. */
|
|
9
|
+
export interface DeliveryOf<Self> {
|
|
10
|
+
readonly "~@kutur-kit/assets/AssetDelivery": Self;
|
|
11
|
+
}
|
|
12
|
+
/** The key a bucket's `Delivery` is provided and required by. */
|
|
13
|
+
export declare const of: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>) => Context.Service<DeliveryOf<Self>, Delivery>;
|
|
14
|
+
/**
|
|
15
|
+
* Public URLs for a bucket's objects under a base URL, such as a CDN or custom
|
|
16
|
+
* domain in front of the storage: `${baseUrl}/${key}`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const layer: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: {
|
|
19
|
+
readonly baseUrl: string;
|
|
20
|
+
}) => Layer.Layer<DeliveryOf<Self>>;
|
|
21
|
+
/** The object's public URL. */
|
|
22
|
+
export declare const url: <Type extends Asset.MediaType, K, B>(asset: Asset.Asset<string, Type, K, B>, key: K) => Effect.Effect<string, Asset.InvalidKey, DeliveryOf<B>>;
|
|
23
|
+
//# sourceMappingURL=AssetDelivery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssetDelivery.d.ts","sources":["../src/AssetDelivery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIhD,2FAA2F;AAC3F,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,wDAAwD;AACxD,MAAM,WAAW,UAAU,CAAC,IAAI;IAC9B,QAAQ,CAAC,kCAAkC,EAAE,IAAI,CAAC;CACnD;AAED,iEAAiE;AACjE,eAAO,MAAM,EAAE,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UAChC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,KAChC,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,CACgD,CAAC;AAE9F;;;GAGG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACnC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KACpC,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CACsD,CAAC;AAEtF,+BAA+B;AAC/B,eAAO,MAAM,GAAG,GAAI,IAAI,SAAS,KAAK,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC,SAC7C,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,OACjC,CAAC,KACL,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,CAGrD,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { encodeKey } from "./internal/url.js";
|
|
3
|
+
/** The key a bucket's `Delivery` is provided and required by. */
|
|
4
|
+
export const of = (bucket) => Context.Service(`@kutur-kit/assets/AssetDelivery/${bucket.id}`);
|
|
5
|
+
/**
|
|
6
|
+
* Public URLs for a bucket's objects under a base URL, such as a CDN or custom
|
|
7
|
+
* domain in front of the storage: `${baseUrl}/${key}`.
|
|
8
|
+
*/
|
|
9
|
+
export const layer = (bucket, options) => Layer.succeed(of(bucket), { url: (key) => `${options.baseUrl}/${encodeKey(key)}` });
|
|
10
|
+
/** The object's public URL. */
|
|
11
|
+
export const url = (asset, key) => Effect.flatMap(asset.storedKey(key), (stored) => of(asset.bucket).useSync((delivery) => delivery.url(stored)));
|
|
12
|
+
//# sourceMappingURL=AssetDelivery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssetDelivery.js","sources":["../src/AssetDelivery.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAY9C,iEAAiE;AACjE,MAAM,CAAC,MAAM,EAAE,GAAG,CAChB,MAAiC,EACY,EAAE,CAC/C,OAAO,CAAC,OAAO,CAA6B,mCAAmC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC;AAE9F;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,CACnB,MAAiC,EACjC,OAAqC,EACN,EAAE,CACjC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,OAAO,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;AAEtF,+BAA+B;AAC/B,MAAM,CAAC,MAAM,GAAG,GAAG,CACjB,KAAsC,EACtC,GAAM,EACkD,EAAE,CAC1D,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAC9C,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAC7D,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type * as Asset from "./Asset.ts";
|
|
2
|
+
import type * as AssetBucket from "./AssetBucket.ts";
|
|
3
|
+
import { ByteSize, Context, Duration, Effect, Schema } from "effect";
|
|
4
|
+
/**
|
|
5
|
+
* A request a client sends to storage itself, such as a browser uploading a
|
|
6
|
+
* file without passing it through the app. A schema, so APIs can return it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SignedRequest: Schema.Struct<{
|
|
9
|
+
readonly method: Schema.Literals<readonly ["PUT", "GET"]>;
|
|
10
|
+
readonly url: Schema.String;
|
|
11
|
+
/**
|
|
12
|
+
* Headers the client must send as given: they are part of the signature.
|
|
13
|
+
* Uploads use only `uploadHeaders`.
|
|
14
|
+
*/
|
|
15
|
+
readonly headers: Schema.$Record<Schema.String, Schema.String>;
|
|
16
|
+
readonly expiresAt: Schema.DateTimeUtc;
|
|
17
|
+
}>;
|
|
18
|
+
export type SignedRequest = typeof SignedRequest.Type;
|
|
19
|
+
/**
|
|
20
|
+
* The headers a signed upload may carry. Browsers send a CORS preflight
|
|
21
|
+
* before uploading with them to storage on another origin, so the bucket's
|
|
22
|
+
* CORS rules must allow them, not only `content-type`:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* { allowedOrigins: [appOrigin], allowedMethods: ["PUT"], allowedHeaders: [...AssetSigning.uploadHeaders] }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare const uploadHeaders: readonly ["content-type", "if-none-match", "cache-control"];
|
|
29
|
+
export interface SignPutInput {
|
|
30
|
+
readonly key: string;
|
|
31
|
+
readonly contentType: string;
|
|
32
|
+
/** The exact length the upload must have. */
|
|
33
|
+
readonly contentLength: ByteSize.ByteSize;
|
|
34
|
+
/** Stored with the object; the client must send it as given. */
|
|
35
|
+
readonly cacheControl: string;
|
|
36
|
+
readonly expiresIn: Duration.Duration;
|
|
37
|
+
}
|
|
38
|
+
export interface SignGetInput {
|
|
39
|
+
readonly key: string;
|
|
40
|
+
readonly expiresIn: Duration.Duration;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Signs requests that clients send to a bucket's storage directly. Storage
|
|
44
|
+
* must refuse a signed request that differs from what was signed: another
|
|
45
|
+
* method, key, content type, length or cache control, or one sent after it
|
|
46
|
+
* expires. Signed uploads create an object and never replace one, so a URL
|
|
47
|
+
* replayed after its upload is refused.
|
|
48
|
+
*/
|
|
49
|
+
export interface Signing {
|
|
50
|
+
readonly signPut: (input: SignPutInput) => Effect.Effect<SignedRequest, AssetBucket.AssetBucketError>;
|
|
51
|
+
readonly signGet: (input: SignGetInput) => Effect.Effect<SignedRequest, AssetBucket.AssetBucketError>;
|
|
52
|
+
}
|
|
53
|
+
/** The service a bucket's `Signing` is provided as. */
|
|
54
|
+
export interface SigningOf<Self> {
|
|
55
|
+
readonly "~@kutur-kit/assets/AssetSigning": Self;
|
|
56
|
+
}
|
|
57
|
+
/** The key a bucket's `Signing` is provided and required by. */
|
|
58
|
+
export declare const of: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>) => Context.Service<SigningOf<Self>, Signing>;
|
|
59
|
+
declare const UploadNotConfirmed_base: Schema.Class<UploadNotConfirmed, Schema.TaggedStruct<"AssetUploadNotConfirmed", {
|
|
60
|
+
readonly asset: Schema.String;
|
|
61
|
+
readonly reason: Schema.Literals<readonly ["missing", "size", "contentType"]>;
|
|
62
|
+
}>, import("effect/Cause").YieldableError & {
|
|
63
|
+
readonly brand: unique symbol;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* The stored object does not match the upload it should hold: it is missing,
|
|
67
|
+
* or its size or content type differs from what the client declared.
|
|
68
|
+
*/
|
|
69
|
+
export declare class UploadNotConfirmed extends UploadNotConfirmed_base {
|
|
70
|
+
}
|
|
71
|
+
/** An upload a client declares before sending it: its content type and size. */
|
|
72
|
+
export interface Upload<Type extends Asset.MediaType> {
|
|
73
|
+
readonly contentType: Type;
|
|
74
|
+
readonly size: ByteSize.ByteSize;
|
|
75
|
+
}
|
|
76
|
+
declare const Size: Schema.decodeTo<Schema.ByteSize, Schema.Int, never, never>;
|
|
77
|
+
/** The schema of an upload declaration, a struct apps can extend with its `fields`. */
|
|
78
|
+
export type UploadSchema<Type extends Asset.MediaType> = Schema.Struct<{
|
|
79
|
+
readonly contentType: Schema.Codec<Type, string>;
|
|
80
|
+
readonly size: typeof Size;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Decodes an upload a client declares, such as a request body, with a content
|
|
84
|
+
* type the asset accepts. Sizes over its `maxSize` are refused by
|
|
85
|
+
* `presignPut`, as `Asset.TooLarge`. Extend it for requests that carry more:
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* Schema.Struct({ ...AssetSigning.Upload(ProjectFile).fields, name: Schema.String })
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare const Upload: <Type extends Asset.MediaType, K, B>(asset: Asset.Asset<string, Type, K, B>) => UploadSchema<Type>;
|
|
92
|
+
export interface SignOptions {
|
|
93
|
+
/** How long the URL stays valid. 15 minutes by default. */
|
|
94
|
+
readonly expiresIn?: Duration.Input | undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Signs a URL the client uploads the object to, bound to its content type,
|
|
98
|
+
* size and the asset's cache control.
|
|
99
|
+
*/
|
|
100
|
+
export declare const presignPut: <Type extends Asset.MediaType, K, B>(asset: Asset.Asset<string, Type, K, B>, key: K, upload: Upload<Type>, options?: SignOptions) => Effect.Effect<SignedRequest, Asset.InvalidKey | Asset.UnsupportedContentType | Asset.TooLarge | AssetBucket.AssetBucketError, SigningOf<B>>;
|
|
101
|
+
/** Signs a URL the client downloads the object from. */
|
|
102
|
+
export declare const presignGet: <Type extends Asset.MediaType, K, B>(asset: Asset.Asset<string, Type, K, B>, key: K, options?: SignOptions) => Effect.Effect<SignedRequest, Asset.InvalidKey | AssetBucket.AssetBucketError, SigningOf<B>>;
|
|
103
|
+
/**
|
|
104
|
+
* Checks that a client's upload arrived as declared, before the app records
|
|
105
|
+
* it: the object exists with the declared size and content type.
|
|
106
|
+
*/
|
|
107
|
+
export declare const confirm: <Type extends Asset.MediaType, K, B>(asset: Asset.Asset<string, Type, K, B>, key: K, upload: Upload<Type>) => Effect.Effect<Asset.Info<Type>, Asset.InvalidKey | UploadNotConfirmed | AssetBucket.AssetBucketError, B>;
|
|
108
|
+
export {};
|
|
109
|
+
//# sourceMappingURL=AssetSigning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssetSigning.d.ts","sources":["../src/AssetSigning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ByteSize, Context, Duration, Effect, Option, Schema, SchemaGetter } from "effect";
|
|
2
|
+
/**
|
|
3
|
+
* A request a client sends to storage itself, such as a browser uploading a
|
|
4
|
+
* file without passing it through the app. A schema, so APIs can return it.
|
|
5
|
+
*/
|
|
6
|
+
export const SignedRequest = Schema.Struct({
|
|
7
|
+
method: Schema.Literals(["PUT", "GET"]),
|
|
8
|
+
url: Schema.String,
|
|
9
|
+
/**
|
|
10
|
+
* Headers the client must send as given: they are part of the signature.
|
|
11
|
+
* Uploads use only `uploadHeaders`.
|
|
12
|
+
*/
|
|
13
|
+
headers: Schema.Record(Schema.String, Schema.String),
|
|
14
|
+
expiresAt: Schema.DateTimeUtc,
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* The headers a signed upload may carry. Browsers send a CORS preflight
|
|
18
|
+
* before uploading with them to storage on another origin, so the bucket's
|
|
19
|
+
* CORS rules must allow them, not only `content-type`:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* { allowedOrigins: [appOrigin], allowedMethods: ["PUT"], allowedHeaders: [...AssetSigning.uploadHeaders] }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const uploadHeaders = ["content-type", "if-none-match", "cache-control"];
|
|
26
|
+
/** The key a bucket's `Signing` is provided and required by. */
|
|
27
|
+
export const of = (bucket) => Context.Service(`@kutur-kit/assets/AssetSigning/${bucket.id}`);
|
|
28
|
+
/**
|
|
29
|
+
* The stored object does not match the upload it should hold: it is missing,
|
|
30
|
+
* or its size or content type differs from what the client declared.
|
|
31
|
+
*/
|
|
32
|
+
export class UploadNotConfirmed extends Schema.TaggedError()("AssetUploadNotConfirmed", { asset: Schema.String, reason: Schema.Literals(["missing", "size", "contentType"]) }, { httpApiStatus: 409 }) {
|
|
33
|
+
}
|
|
34
|
+
// A size in a JSON body: a whole number of bytes.
|
|
35
|
+
const Size = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.decodeTo(Schema.ByteSize, {
|
|
36
|
+
decode: SchemaGetter.transform((size) => ByteSize.bytes(size)),
|
|
37
|
+
encode: SchemaGetter.transform((size) => Number(size)),
|
|
38
|
+
}));
|
|
39
|
+
/**
|
|
40
|
+
* Decodes an upload a client declares, such as a request body, with a content
|
|
41
|
+
* type the asset accepts. Sizes over its `maxSize` are refused by
|
|
42
|
+
* `presignPut`, as `Asset.TooLarge`. Extend it for requests that carry more:
|
|
43
|
+
*
|
|
44
|
+
* ```ts
|
|
45
|
+
* Schema.Struct({ ...AssetSigning.Upload(ProjectFile).fields, name: Schema.String })
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export const Upload = (asset) => Schema.Struct({ contentType: asset.ContentType, size: Size });
|
|
49
|
+
const expiry = (options) => Duration.fromInputUnsafe(options?.expiresIn ?? "15 minutes");
|
|
50
|
+
/**
|
|
51
|
+
* Signs a URL the client uploads the object to, bound to its content type,
|
|
52
|
+
* size and the asset's cache control.
|
|
53
|
+
*/
|
|
54
|
+
export const presignPut = (asset, key, upload, options) => Effect.gen(function* () {
|
|
55
|
+
const stored = yield* asset.storedKey(key);
|
|
56
|
+
yield* asset.check({ contentType: upload.contentType, contentLength: upload.size });
|
|
57
|
+
const signing = yield* of(asset.bucket);
|
|
58
|
+
return yield* signing.signPut({
|
|
59
|
+
key: stored,
|
|
60
|
+
contentType: upload.contentType,
|
|
61
|
+
contentLength: upload.size,
|
|
62
|
+
cacheControl: asset.cacheControl,
|
|
63
|
+
expiresIn: expiry(options),
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
/** Signs a URL the client downloads the object from. */
|
|
67
|
+
export const presignGet = (asset, key, options) => Effect.gen(function* () {
|
|
68
|
+
const stored = yield* asset.storedKey(key);
|
|
69
|
+
const signing = yield* of(asset.bucket);
|
|
70
|
+
return yield* signing.signGet({ key: stored, expiresIn: expiry(options) });
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Checks that a client's upload arrived as declared, before the app records
|
|
74
|
+
* it: the object exists with the declared size and content type.
|
|
75
|
+
*/
|
|
76
|
+
export const confirm = (asset, key, upload) => Effect.gen(function* () {
|
|
77
|
+
const stored = yield* asset.storedKey(key);
|
|
78
|
+
const bucket = yield* asset.bucket;
|
|
79
|
+
const info = yield* bucket.head(stored);
|
|
80
|
+
const reason = Option.match(info, {
|
|
81
|
+
onNone: () => Option.some("missing"),
|
|
82
|
+
onSome: ({ size, contentType }) => size !== upload.size
|
|
83
|
+
? Option.some("size")
|
|
84
|
+
: contentType === upload.contentType
|
|
85
|
+
? Option.none()
|
|
86
|
+
: Option.some("contentType"),
|
|
87
|
+
});
|
|
88
|
+
return yield* Option.match(reason, {
|
|
89
|
+
onNone: () => Effect.succeed({ size: upload.size, contentType: upload.contentType }),
|
|
90
|
+
onSome: (because) => Effect.fail(new UploadNotConfirmed({ asset: asset.id, reason: because })),
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
//# sourceMappingURL=AssetSigning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AssetSigning.js","sources":["../src/AssetSigning.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type * as AssetSigning from "../AssetSigning.ts";
|
|
2
|
+
import type { Bucket, BucketCorsRule } from "@alchemy.run/cloudflare/R2";
|
|
3
|
+
import type { WorkerEnvironment } from "@alchemy.run/cloudflare/Workers";
|
|
4
|
+
import { ReadWriteBucket } from "@alchemy.run/cloudflare/R2";
|
|
5
|
+
import { Effect, Layer } from "effect";
|
|
6
|
+
import * as AssetBucket from "../AssetBucket.ts";
|
|
7
|
+
/** An R2 bucket resource, or the Effect that declares it. */
|
|
8
|
+
export type R2Bucket<R = never> = Bucket | Effect.Effect<Bucket, never, R>;
|
|
9
|
+
/**
|
|
10
|
+
* Backs a bucket with an R2 bucket through alchemy's `ReadWriteBucket`.
|
|
11
|
+
* Provide its implementation for where the app runs: `ReadWriteBucketBinding`
|
|
12
|
+
* in a Worker, `ReadWriteBucketHttp` elsewhere alchemy deploys. Content type
|
|
13
|
+
* and cache control are stored as the object's HTTP metadata, which R2
|
|
14
|
+
* serves on its public domains.
|
|
15
|
+
*
|
|
16
|
+
* Bodies are collected before they are sent, so a failing body sends
|
|
17
|
+
* nothing. Uploads through the app are small by `maxSize`; larger files go
|
|
18
|
+
* to storage directly, through `signing`.
|
|
19
|
+
*/
|
|
20
|
+
export declare const layer: <Self, Id extends string, R = never>(bucket: AssetBucket.Tag<Self, Id>, r2: R2Bucket<R>) => Layer.Layer<Self, never, ReadWriteBucket | R>;
|
|
21
|
+
/**
|
|
22
|
+
* Signs URLs for an R2 bucket from a Worker, with the S3 credentials alchemy
|
|
23
|
+
* derives for it: a token scoped to the bucket when deployed, the local S3
|
|
24
|
+
* endpoint under `alchemy dev`. Signed as `S3Assets.signing` signs: uploads
|
|
25
|
+
* are bound to their content type, length and cache control, and write once.
|
|
26
|
+
*
|
|
27
|
+
* Outside a Worker, use `S3Assets.signing` with `S3Assets.r2Endpoint` and
|
|
28
|
+
* the S3 keys of an R2 API token.
|
|
29
|
+
*/
|
|
30
|
+
export declare const signing: <Self, Id extends string, R = never>(bucket: AssetBucket.Tag<Self, Id>, r2: R2Bucket<R>) => Layer.Layer<AssetSigning.SigningOf<Self>, never, WorkerEnvironment | R>;
|
|
31
|
+
/**
|
|
32
|
+
* The CORS rule an R2 bucket needs for browsers on `origins` to upload to it
|
|
33
|
+
* with signed URLs: `PUT` with `AssetSigning.uploadHeaders`.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* Cloudflare.R2.Bucket("Uploads", { cors: [AssetsR2.corsRule(["https://app.example.com"])] })
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare const corsRule: (origins: ReadonlyArray<string>) => BucketCorsRule;
|
|
40
|
+
//# sourceMappingURL=r2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.d.ts","sources":["../../src/alchemy/r2.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,YAAY,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAwB,MAAM,4BAA4B,CAAC;AAC/F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AAEzE,OAAO,EAAE,eAAe,EAAiB,MAAM,4BAA4B,CAAC;AAG5E,OAAO,EAAY,MAAM,EAAE,KAAK,EAAkB,MAAM,QAAQ,CAAC;AAEjE,OAAO,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAKjD,6DAA6D;AAC7D,MAAM,MAAM,QAAQ,CAAC,CAAC,GAAG,KAAK,IAAI,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAkB3E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC,GAAG,KAAK,UAC9C,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,MAC7B,QAAQ,CAAC,CAAC,CAAC,KACd,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,eAAe,GAAG,CAAC,CAuD5C,CAAC;AAEJ;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC,GAAG,KAAK,UAChD,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,MAC7B,QAAQ,CAAC,CAAC,CAAC,KACd,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,GAAG,CAAC,CAkBtE,CAAC;AAEJ;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,YAAa,aAAa,CAAC,MAAM,CAAC,KAAG,cAIxD,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ReadWriteBucket, S3Credentials } from "@alchemy.run/cloudflare/R2";
|
|
2
|
+
import * as Credentials from "@distilled.cloud/aws/Credentials";
|
|
3
|
+
import { RuntimeContext } from "alchemy";
|
|
4
|
+
import { ByteSize, Effect, Layer, Option, Stream } from "effect";
|
|
5
|
+
import * as AssetBucket from "../AssetBucket.js";
|
|
6
|
+
import * as AssetSigningModule from "../AssetSigning.js";
|
|
7
|
+
import { concat } from "../internal/bytes.js";
|
|
8
|
+
import * as SigV4 from "../internal/sigv4.js";
|
|
9
|
+
// Alchemy marks binding methods as callable only while handling a request.
|
|
10
|
+
// `Bucket` methods carry no requirements, so the marker is satisfied here,
|
|
11
|
+
// as alchemy does where it implements other interfaces. The methods read
|
|
12
|
+
// nothing from it, but they fail if called while a Worker is constructed.
|
|
13
|
+
const handling = (effect) => Effect.provide(effect, RuntimeContext.phantom);
|
|
14
|
+
const infoOf = (object) => ({
|
|
15
|
+
size: ByteSize.bytes(object.size),
|
|
16
|
+
contentType: object.httpMetadata?.contentType ?? "application/octet-stream",
|
|
17
|
+
cacheControl: object.httpMetadata?.cacheControl,
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Backs a bucket with an R2 bucket through alchemy's `ReadWriteBucket`.
|
|
21
|
+
* Provide its implementation for where the app runs: `ReadWriteBucketBinding`
|
|
22
|
+
* in a Worker, `ReadWriteBucketHttp` elsewhere alchemy deploys. Content type
|
|
23
|
+
* and cache control are stored as the object's HTTP metadata, which R2
|
|
24
|
+
* serves on its public domains.
|
|
25
|
+
*
|
|
26
|
+
* Bodies are collected before they are sent, so a failing body sends
|
|
27
|
+
* nothing. Uploads through the app are small by `maxSize`; larger files go
|
|
28
|
+
* to storage directly, through `signing`.
|
|
29
|
+
*/
|
|
30
|
+
export const layer = (bucket, r2) => Layer.effect(bucket, Effect.gen(function* () {
|
|
31
|
+
const client = yield* ReadWriteBucket(r2);
|
|
32
|
+
const fail = (operation, key) => Effect.mapError((cause) => new AssetBucket.AssetBucketError({ operation, bucket: bucket.id, key, cause }));
|
|
33
|
+
return bucket.of({
|
|
34
|
+
put: ({ key, contentType, cacheControl, body }) => Effect.gen(function* () {
|
|
35
|
+
const bytes = concat(yield* Stream.runCollect(body));
|
|
36
|
+
const httpMetadata = { contentType };
|
|
37
|
+
if (cacheControl !== undefined)
|
|
38
|
+
httpMetadata.cacheControl = cacheControl;
|
|
39
|
+
const object = yield* handling(client.put(key, bytes, { httpMetadata })).pipe(fail("put", key));
|
|
40
|
+
return { ...infoOf(object), contentType, cacheControl };
|
|
41
|
+
}),
|
|
42
|
+
get: (key) => handling(client.get(key)).pipe(Effect.map((object) => object === null
|
|
43
|
+
? Option.none()
|
|
44
|
+
: Option.some({
|
|
45
|
+
info: infoOf(object),
|
|
46
|
+
body: Stream.mapError(object.body, (cause) => new AssetBucket.AssetBucketError({
|
|
47
|
+
operation: "get",
|
|
48
|
+
bucket: bucket.id,
|
|
49
|
+
key,
|
|
50
|
+
cause,
|
|
51
|
+
})),
|
|
52
|
+
})), fail("get", key)),
|
|
53
|
+
head: (key) => handling(client.head(key)).pipe(Effect.map((object) => (object === null ? Option.none() : Option.some(infoOf(object)))), fail("head", key)),
|
|
54
|
+
delete: (key) => handling(client.delete(key)).pipe(fail("delete", key)),
|
|
55
|
+
});
|
|
56
|
+
}));
|
|
57
|
+
/**
|
|
58
|
+
* Signs URLs for an R2 bucket from a Worker, with the S3 credentials alchemy
|
|
59
|
+
* derives for it: a token scoped to the bucket when deployed, the local S3
|
|
60
|
+
* endpoint under `alchemy dev`. Signed as `S3Assets.signing` signs: uploads
|
|
61
|
+
* are bound to their content type, length and cache control, and write once.
|
|
62
|
+
*
|
|
63
|
+
* Outside a Worker, use `S3Assets.signing` with `S3Assets.r2Endpoint` and
|
|
64
|
+
* the S3 keys of an R2 API token.
|
|
65
|
+
*/
|
|
66
|
+
export const signing = (bucket, r2) => Layer.effect(AssetSigningModule.of(bucket), Effect.gen(function* () {
|
|
67
|
+
const credentials = handling(yield* S3Credentials(r2, { access: "read-write" }));
|
|
68
|
+
return yield* SigV4.make(bucket.id, Effect.map(credentials, ({ endpoint, bucketName }) => ({ endpoint, bucket: bucketName }))).pipe(Effect.provideService(Credentials.Credentials, Effect.map(credentials, ({ accessKeyId, secretAccessKey, region }) => Credentials.fromAwsCredentialIdentity({ accessKeyId, secretAccessKey }, region))));
|
|
69
|
+
}));
|
|
70
|
+
/**
|
|
71
|
+
* The CORS rule an R2 bucket needs for browsers on `origins` to upload to it
|
|
72
|
+
* with signed URLs: `PUT` with `AssetSigning.uploadHeaders`.
|
|
73
|
+
*
|
|
74
|
+
* ```ts
|
|
75
|
+
* Cloudflare.R2.Bucket("Uploads", { cors: [AssetsR2.corsRule(["https://app.example.com"])] })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const corsRule = (origins) => ({
|
|
79
|
+
allowedMethods: ["PUT"],
|
|
80
|
+
allowedOrigins: [...origins],
|
|
81
|
+
allowedHeaders: [...AssetSigningModule.uploadHeaders],
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=r2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.js","sources":["../../src/alchemy/r2.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as AssetBucket from "../AssetBucket.ts";
|
|
2
|
+
import { HttpRouter } from "effect/http";
|
|
3
|
+
export interface Options {
|
|
4
|
+
/**
|
|
5
|
+
* Where `routes` are mounted, such as `/cdn/public` or
|
|
6
|
+
* `http://localhost:3000/cdn/public`. Give `AssetDelivery.layer` the same
|
|
7
|
+
* base URL, so the public URLs it makes reach the routes.
|
|
8
|
+
*/
|
|
9
|
+
readonly baseUrl: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Serves every object of a bucket under `baseUrl` without a signature, as a
|
|
13
|
+
* CDN serves a public bucket, for development and tests. Objects are served
|
|
14
|
+
* with the cache control stored with them, as R2 and S3 serve it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const routes: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: Options) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", Exclude<Self, HttpRouter.Provided>>>;
|
|
17
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sources":["../../src/delivery/local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,WAAW,MAAM,mBAAmB,CAAC;AAGtD,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAI7D,MAAM,WAAW,OAAO;IACtB;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,eAAO,MAAM,MAAM,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACpC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB,OAAO,gJAiBjB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
|
+
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
3
|
+
import { mount, respond, target } from "../internal/http.js";
|
|
4
|
+
/**
|
|
5
|
+
* Serves every object of a bucket under `baseUrl` without a signature, as a
|
|
6
|
+
* CDN serves a public bucket, for development and tests. Objects are served
|
|
7
|
+
* with the cache control stored with them, as R2 and S3 serve it.
|
|
8
|
+
*/
|
|
9
|
+
export const routes = (bucket, options) => {
|
|
10
|
+
const { prefix, path } = mount(options.baseUrl);
|
|
11
|
+
return HttpRouter.add("GET", path, Effect.gen(function* () {
|
|
12
|
+
const { key } = yield* target(prefix);
|
|
13
|
+
const store = yield* bucket;
|
|
14
|
+
return yield* Option.match(key, {
|
|
15
|
+
onNone: () => Effect.succeed(HttpServerResponse.empty({ status: 404 })),
|
|
16
|
+
onSome: (value) => respond(store, value),
|
|
17
|
+
});
|
|
18
|
+
}).pipe(Effect.orDie));
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sources":["../../src/delivery/local.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAW7D;;;;GAIG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,CACpB,MAAiC,EACjC,OAAgB,EAChB,EAAE;IACF,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEhD,OAAO,UAAU,CAAC,GAAG,CACnB,KAAK,EACL,IAAI,EACJ,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC;QAE5B,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE;YAC9B,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACvE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;SACzC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CACtB,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FileSystem, Layer, Path } from "effect";
|
|
2
|
+
import * as AssetBucket from "./AssetBucket.ts";
|
|
3
|
+
export interface Options {
|
|
4
|
+
/** The directory holding the bucket's files. It is created when needed. */
|
|
5
|
+
readonly directory: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Backs a bucket with files under a directory, for development and tests
|
|
9
|
+
* without object storage. Keys are stored by their SHA-256, so any key fits
|
|
10
|
+
* any file system: case-insensitive ones and Windows included. Writes are
|
|
11
|
+
* atomic for one process; concurrent writers to one key may leave unused
|
|
12
|
+
* files behind.
|
|
13
|
+
*/
|
|
14
|
+
export declare const layer: <Self, Id extends string>(bucket: AssetBucket.Tag<Self, Id>, options: Options) => Layer.Layer<Self, never, FileSystem.FileSystem | Path.Path>;
|
|
15
|
+
//# sourceMappingURL=filesystem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesystem.d.ts","sources":["../src/filesystem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,UAAU,EAAE,KAAK,EAAU,IAAI,EAAkB,MAAM,QAAQ,CAAC;AAEjF,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAEhD,MAAM,WAAW,OAAO;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AA2BD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,GAAI,IAAI,EAAE,EAAE,SAAS,MAAM,UACnC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,WACxB,OAAO,KACf,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAyH1D,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
|
|
2
|
+
import * as AssetBucket from "./AssetBucket.js";
|
|
3
|
+
// What a stored object's record holds. `file` names the body's file, a new
|
|
4
|
+
// one for each write, so a replacement is visible only once its record is.
|
|
5
|
+
const Entry = Schema.Struct({
|
|
6
|
+
key: Schema.String,
|
|
7
|
+
contentType: Schema.String,
|
|
8
|
+
cacheControl: Schema.optional(Schema.String),
|
|
9
|
+
size: Schema.ByteSize,
|
|
10
|
+
file: Schema.String,
|
|
11
|
+
});
|
|
12
|
+
const EntryJson = Schema.fromJsonString(Schema.toCodecJson(Entry));
|
|
13
|
+
const infoOf = (entry) => ({
|
|
14
|
+
size: entry.size,
|
|
15
|
+
contentType: entry.contentType,
|
|
16
|
+
cacheControl: entry.cacheControl,
|
|
17
|
+
});
|
|
18
|
+
class BodyFailure {
|
|
19
|
+
error;
|
|
20
|
+
constructor(error) {
|
|
21
|
+
this.error = error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const hex = (bytes) => Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
25
|
+
/**
|
|
26
|
+
* Backs a bucket with files under a directory, for development and tests
|
|
27
|
+
* without object storage. Keys are stored by their SHA-256, so any key fits
|
|
28
|
+
* any file system: case-insensitive ones and Windows included. Writes are
|
|
29
|
+
* atomic for one process; concurrent writers to one key may leave unused
|
|
30
|
+
* files behind.
|
|
31
|
+
*/
|
|
32
|
+
export const layer = (bucket, options) => Layer.effect(bucket, Effect.gen(function* () {
|
|
33
|
+
const fs = yield* FileSystem.FileSystem;
|
|
34
|
+
const path = yield* Path.Path;
|
|
35
|
+
const failure = (operation, key) => (cause) => new AssetBucket.AssetBucketError({ operation, bucket: bucket.id, key, cause });
|
|
36
|
+
// Two hex characters of the hash shard the directory, as Git does.
|
|
37
|
+
const locate = (key) => Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(key))).pipe(Effect.map((digest) => {
|
|
38
|
+
const hash = hex(digest);
|
|
39
|
+
const directory = path.join(options.directory, hash.slice(0, 2));
|
|
40
|
+
return { directory, hash, record: path.join(directory, `${hash}.json`) };
|
|
41
|
+
}));
|
|
42
|
+
const read = (key, operation) => Effect.gen(function* () {
|
|
43
|
+
const { directory, record } = yield* locate(key);
|
|
44
|
+
const json = yield* fs.readFileString(record).pipe(Effect.asSome, Effect.catchReason("PlatformError", "NotFound", () => Effect.succeedNone));
|
|
45
|
+
if (Option.isNone(json))
|
|
46
|
+
return Option.none();
|
|
47
|
+
const entry = yield* Schema.decodeEffect(EntryJson)(json.value);
|
|
48
|
+
return Option.some({ entry, file: path.join(directory, entry.file) });
|
|
49
|
+
}).pipe(Effect.mapError(failure(operation, key)));
|
|
50
|
+
return bucket.of({
|
|
51
|
+
put: ({ key, contentType, cacheControl, body }) => Effect.gen(function* () {
|
|
52
|
+
const { directory, hash, record } = yield* locate(key).pipe(Effect.tap(({ directory }) => fs.makeDirectory(directory, { recursive: true })), Effect.mapError(failure("put", key)));
|
|
53
|
+
const previous = yield* read(key, "put");
|
|
54
|
+
const version = crypto.randomUUID();
|
|
55
|
+
const file = `${hash}.${version}`;
|
|
56
|
+
const target = path.join(directory, file);
|
|
57
|
+
// Body errors pass through the sink, so they are wrapped to keep
|
|
58
|
+
// them apart from the file system's.
|
|
59
|
+
const size = yield* Stream.run(Stream.mapError(body, (error) => new BodyFailure(error)), fs.sink(target)).pipe(Effect.andThen(fs.stat(target)), Effect.mapError((error) => error instanceof BodyFailure ? error.error : failure("put", key)(error)), Effect.map((info) => info.size), Effect.onError(() => Effect.ignore(fs.remove(target, { force: true }))));
|
|
60
|
+
const entry = { key, contentType, cacheControl, size, file };
|
|
61
|
+
const staged = `${record}.${version}`;
|
|
62
|
+
yield* Schema.encodeEffect(EntryJson)(entry).pipe(Effect.flatMap((json) => fs.writeFileString(staged, json)), Effect.andThen(fs.rename(staged, record)), Effect.onError(() => Effect.ignore(Effect.all([
|
|
63
|
+
fs.remove(staged, { force: true }),
|
|
64
|
+
fs.remove(target, { force: true }),
|
|
65
|
+
]))), Effect.mapError(failure("put", key)));
|
|
66
|
+
// The replaced body is no longer referenced. Leaving it on failure
|
|
67
|
+
// costs only disk space.
|
|
68
|
+
yield* Option.match(previous, {
|
|
69
|
+
onNone: () => Effect.void,
|
|
70
|
+
onSome: ({ file }) => Effect.ignore(fs.remove(file, { force: true })),
|
|
71
|
+
});
|
|
72
|
+
return { size: entry.size, contentType, cacheControl };
|
|
73
|
+
}),
|
|
74
|
+
get: (key) => Effect.map(read(key, "get"), Option.map(({ entry, file }) => ({
|
|
75
|
+
info: infoOf(entry),
|
|
76
|
+
body: Stream.mapError(fs.stream(file), failure("get", key)),
|
|
77
|
+
}))),
|
|
78
|
+
head: (key) => Effect.map(read(key, "head"), Option.map(({ entry }) => infoOf(entry))),
|
|
79
|
+
delete: (key) => Effect.gen(function* () {
|
|
80
|
+
const stored = yield* read(key, "delete");
|
|
81
|
+
if (Option.isNone(stored))
|
|
82
|
+
return;
|
|
83
|
+
const { record } = yield* locate(key);
|
|
84
|
+
yield* fs
|
|
85
|
+
.remove(record, { force: true })
|
|
86
|
+
.pipe(Effect.andThen(fs.remove(stored.value.file, { force: true })), Effect.mapError(failure("delete", key)));
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
}));
|
|
90
|
+
//# sourceMappingURL=filesystem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filesystem.js","sources":["../src/filesystem.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAEpC,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAEpC,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.d.ts","sources":["../../src/internal/bytes.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,eAAO,MAAM,MAAM,WAAY,aAAa,CAAC,UAAU,CAAC,KAAG,UAU1D,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Joins chunks into one array. */
|
|
2
|
+
export const concat = (chunks) => {
|
|
3
|
+
const bytes = new Uint8Array(chunks.reduce((size, chunk) => size + chunk.length, 0));
|
|
4
|
+
let offset = 0;
|
|
5
|
+
for (const chunk of chunks) {
|
|
6
|
+
bytes.set(chunk, offset);
|
|
7
|
+
offset += chunk.length;
|
|
8
|
+
}
|
|
9
|
+
return bytes;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=bytes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bytes.js","sources":["../../src/internal/bytes.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAiC,EAAc,EAAE;IACtE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;IACrF,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC;IACzB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC,CAAC"}
|