@crowi/plugin-storage-aws-s3 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +91 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013 Sotaro KARASAWA <sotaro.k@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @crowi/plugin-storage-aws-s3
|
|
2
|
+
|
|
3
|
+
AWS S3 storage driver for Crowi 2.0. Stores page attachments and profile
|
|
4
|
+
pictures in an S3 bucket. Pairs with [`@crowi/plugin-aws`](../plugin-aws)
|
|
5
|
+
for shared region / access-key configuration.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
crowi-admin plugin add @crowi/plugin-storage-aws-s3
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
(or, in dev: `pnpm --filter @crowi/api add -D @crowi/plugin-storage-aws-s3`)
|
|
14
|
+
|
|
15
|
+
The plugin auto-loads its dependency `@crowi/plugin-aws`. You don't need
|
|
16
|
+
to add it explicitly.
|
|
17
|
+
|
|
18
|
+
## Configure
|
|
19
|
+
|
|
20
|
+
### 1. Activate the driver in `crowi.config.json`
|
|
21
|
+
|
|
22
|
+
```jsonc
|
|
23
|
+
{
|
|
24
|
+
"plugins": ["@crowi/plugin-storage-aws-s3"],
|
|
25
|
+
"storage": { "driver": "s3" }
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A server restart is required when `storage.driver` changes — Crowi reads
|
|
30
|
+
this file once at boot.
|
|
31
|
+
|
|
32
|
+
### 2. Fill in credentials in the admin UI
|
|
33
|
+
|
|
34
|
+
Open `/admin/plugins`:
|
|
35
|
+
|
|
36
|
+
- **`@crowi/plugin-aws`** — `region`, `accessKeyId`, `secretAccessKey`
|
|
37
|
+
(the secret is encrypted at rest with `CROWI_ENCRYPTION_KEY`)
|
|
38
|
+
- **`@crowi/plugin-storage-aws-s3`** — `bucket`
|
|
39
|
+
|
|
40
|
+
Leaving `accessKeyId` and `secretAccessKey` blank tells the SDK to use
|
|
41
|
+
its [default credential chain](https://docs.aws.amazon.com/sdkref/latest/guide/standardized-credentials.html#credentialProviderChain)
|
|
42
|
+
(env vars, shared credentials file, EC2 instance role, ECS task role,
|
|
43
|
+
EKS pod identity, etc). This is the recommended setup when running on
|
|
44
|
+
AWS — let the platform vend short-lived credentials, don't store
|
|
45
|
+
long-lived keys in Mongo.
|
|
46
|
+
|
|
47
|
+
### 3. Migrate existing files (if switching from `local`)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
crowi-admin storage copy --from local --to s3 --dry-run # preview
|
|
51
|
+
crowi-admin storage copy --from local --to s3 # actual copy
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The CLI walks the `Attachment` collection and `User.image` URLs, copying
|
|
55
|
+
every key from one driver to the other. Failures are logged and skipped;
|
|
56
|
+
re-running is safe (S3 `PutObject` is overwrite-by-key).
|
|
57
|
+
|
|
58
|
+
## Required IAM permissions
|
|
59
|
+
|
|
60
|
+
Three S3 operations are called by the driver: `PutObject`, `GetObject`,
|
|
61
|
+
`DeleteObject`. Signed URLs are produced locally with the SDK presigner,
|
|
62
|
+
which signs a `GetObjectCommand` — so the **client** consuming the signed
|
|
63
|
+
URL needs `GetObject` (which the driver's IAM principal already has).
|
|
64
|
+
`ListBucket` is **not** needed.
|
|
65
|
+
|
|
66
|
+
A minimal IAM policy you can attach to the IAM user / role Crowi runs as:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"Version": "2012-10-17",
|
|
71
|
+
"Statement": [
|
|
72
|
+
{
|
|
73
|
+
"Sid": "CrowiStorageObjects",
|
|
74
|
+
"Effect": "Allow",
|
|
75
|
+
"Action": [
|
|
76
|
+
"s3:PutObject",
|
|
77
|
+
"s3:GetObject",
|
|
78
|
+
"s3:DeleteObject"
|
|
79
|
+
],
|
|
80
|
+
"Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The same policy is shipped at [`examples/iam-policy.json`](examples/iam-policy.json)
|
|
87
|
+
so you can apply it directly:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
aws iam create-policy \
|
|
91
|
+
--policy-name CrowiStorage \
|
|
92
|
+
--policy-document file://examples/iam-policy.json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A variant with `s3:ListBucket` enabled is at
|
|
96
|
+
[`examples/iam-policy-with-list.json`](examples/iam-policy-with-list.json) —
|
|
97
|
+
not needed today, but reserved for a future `crowi-admin storage list` /
|
|
98
|
+
`storage diff` command that would enumerate keys server-side.
|
|
99
|
+
|
|
100
|
+
## What does NOT need to be configured
|
|
101
|
+
|
|
102
|
+
- **Bucket policy** — IAM policy is enough for normal single-account
|
|
103
|
+
setups. Use a bucket policy only if you need cross-account access,
|
|
104
|
+
SSE enforcement, or public-read overrides — Crowi neither requires
|
|
105
|
+
nor opposes those, but they're environment-specific and out of scope
|
|
106
|
+
for this plugin's docs.
|
|
107
|
+
- **CORS** — Crowi serves signed URLs directly via `<img>` / download
|
|
108
|
+
links, which are simple `GET` requests with no preflight. CORS rules
|
|
109
|
+
on the bucket are unnecessary unless you add a future feature that
|
|
110
|
+
uploads from the browser straight to S3 (presigned `PUT`).
|
|
111
|
+
- **SSE-S3** — works out of the box if the bucket has default
|
|
112
|
+
encryption enabled. The driver does not set `ServerSideEncryption`
|
|
113
|
+
explicitly; let the bucket's default policy decide.
|
|
114
|
+
|
|
115
|
+
## Object layout
|
|
116
|
+
|
|
117
|
+
The driver passes storage keys through to S3 verbatim. The keys Crowi
|
|
118
|
+
uses are:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
attachment/<pageId>/<fileId>/<original-filename>
|
|
122
|
+
user/<userId>.<ext>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This matches the v1.x layout, so operators upgrading from Crowi 1.x can
|
|
126
|
+
point `bucket` at their existing bucket and files round-trip without
|
|
127
|
+
migration.
|
|
128
|
+
|
|
129
|
+
## Troubleshooting
|
|
130
|
+
|
|
131
|
+
| Symptom | Likely cause |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `bucket=<unset>` in the boot log | Set `bucket` under `/admin/plugins` for `@crowi/plugin-storage-aws-s3`. |
|
|
134
|
+
| `region=<default>` in the boot log | Set `region` under `/admin/plugins` for `@crowi/plugin-aws` (e.g. `ap-northeast-1`). |
|
|
135
|
+
| `403 Forbidden` on PUT | Check the IAM policy — `s3:PutObject` against `arn:aws:s3:::<bucket>/*`. |
|
|
136
|
+
| `403 Forbidden` on GET via signed URL | Same; the signing principal needs `s3:GetObject`. |
|
|
137
|
+
| `301 PermanentRedirect` | `region` mismatch. The bucket lives in a different region than the SDK is configured to talk to. |
|
|
138
|
+
| Long-lived secret keys committed somewhere | Don't. Use IAM roles on EC2 / ECS / EKS, leave `accessKeyId` blank. |
|
|
139
|
+
|
|
140
|
+
## See also
|
|
141
|
+
|
|
142
|
+
- [`@crowi/plugin-aws`](../plugin-aws) — shared AWS credential plugin (auto-loaded as a dependency).
|
|
143
|
+
- [`@crowi/plugin-storage-local`](../plugin-storage-local) — the default-on local filesystem driver.
|
|
144
|
+
- RFC-0001 §"Storage (S3)" for the migration story from Crowi 1.x.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { StorageDriver, CrowiPlugin } from '@crowi/plugin-api';
|
|
3
|
+
|
|
4
|
+
interface S3DriverState {
|
|
5
|
+
client: S3Client;
|
|
6
|
+
bucket: string;
|
|
7
|
+
}
|
|
8
|
+
declare const plugin: CrowiPlugin;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the storage driver. Methods read `driverState` *once at the
|
|
12
|
+
* top* — a snapshot — so a `reconfigure` running concurrently with an
|
|
13
|
+
* inflight `put` / `get` cannot swap the client mid-call. The next
|
|
14
|
+
* call sees the new client/bucket.
|
|
15
|
+
*/
|
|
16
|
+
declare function createS3Driver(driverState: S3DriverState): StorageDriver;
|
|
17
|
+
|
|
18
|
+
export { type S3DriverState, createS3Driver, plugin as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { StorageDriver, CrowiPlugin } from '@crowi/plugin-api';
|
|
3
|
+
|
|
4
|
+
interface S3DriverState {
|
|
5
|
+
client: S3Client;
|
|
6
|
+
bucket: string;
|
|
7
|
+
}
|
|
8
|
+
declare const plugin: CrowiPlugin;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the storage driver. Methods read `driverState` *once at the
|
|
12
|
+
* top* — a snapshot — so a `reconfigure` running concurrently with an
|
|
13
|
+
* inflight `put` / `get` cannot swap the client mid-call. The next
|
|
14
|
+
* call sees the new client/bucket.
|
|
15
|
+
*/
|
|
16
|
+
declare function createS3Driver(driverState: S3DriverState): StorageDriver;
|
|
17
|
+
|
|
18
|
+
export { type S3DriverState, createS3Driver, plugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createS3Driver: () => createS3Driver,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_v3 = require("zod/v3");
|
|
28
|
+
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
29
|
+
var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
|
|
30
|
+
var S3StorageConfigSchema = import_v3.z.object({
|
|
31
|
+
bucket: import_v3.z.string().default("")
|
|
32
|
+
}).strict();
|
|
33
|
+
var state = {
|
|
34
|
+
client: new import_client_s3.S3Client({}),
|
|
35
|
+
bucket: ""
|
|
36
|
+
};
|
|
37
|
+
var plugin = {
|
|
38
|
+
name: "@crowi/plugin-storage-aws-s3",
|
|
39
|
+
version: "0.1.0-dev",
|
|
40
|
+
requires: ["@crowi/plugin-aws"],
|
|
41
|
+
configSchema: S3StorageConfigSchema,
|
|
42
|
+
adminPlacement: {
|
|
43
|
+
label: "AWS S3",
|
|
44
|
+
icon: "cloud"
|
|
45
|
+
// section omitted: derived from registerStorage → 'storage'
|
|
46
|
+
},
|
|
47
|
+
registerStorage: (registry, ctx) => {
|
|
48
|
+
applyConfigToState(ctx, state);
|
|
49
|
+
registry.register("s3", createS3Driver(state));
|
|
50
|
+
ctx.log.debug("registered s3 storage driver (bucket=%s)", state.bucket || "<unset>");
|
|
51
|
+
},
|
|
52
|
+
reconfigure: (ctx) => {
|
|
53
|
+
applyConfigToState(ctx, state);
|
|
54
|
+
ctx.log.debug("reconfigured s3 storage driver (bucket=%s)", state.bucket || "<unset>");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var index_default = plugin;
|
|
58
|
+
function applyConfigToState(ctx, target) {
|
|
59
|
+
const own = ctx.config();
|
|
60
|
+
const aws = ctx.dependencyConfig("@crowi/plugin-aws");
|
|
61
|
+
target.client = new import_client_s3.S3Client({
|
|
62
|
+
region: aws.region || void 0,
|
|
63
|
+
credentials: aws.accessKeyId && aws.secretAccessKey ? {
|
|
64
|
+
accessKeyId: aws.accessKeyId,
|
|
65
|
+
secretAccessKey: aws.secretAccessKey
|
|
66
|
+
} : void 0
|
|
67
|
+
});
|
|
68
|
+
target.bucket = own.bucket;
|
|
69
|
+
}
|
|
70
|
+
function createS3Driver(driverState) {
|
|
71
|
+
return {
|
|
72
|
+
async put(key, body, meta) {
|
|
73
|
+
const { client, bucket } = driverState;
|
|
74
|
+
requireBucket(bucket);
|
|
75
|
+
await client.send(
|
|
76
|
+
new import_client_s3.PutObjectCommand({
|
|
77
|
+
Bucket: bucket,
|
|
78
|
+
Key: key,
|
|
79
|
+
Body: body,
|
|
80
|
+
ContentType: meta.contentType
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
return { key };
|
|
84
|
+
},
|
|
85
|
+
async get(key) {
|
|
86
|
+
const { client, bucket } = driverState;
|
|
87
|
+
requireBucket(bucket);
|
|
88
|
+
const response = await client.send(new import_client_s3.GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
89
|
+
const body = response.Body;
|
|
90
|
+
if (!body) {
|
|
91
|
+
throw new Error(`S3 returned empty body for key '${key}'`);
|
|
92
|
+
}
|
|
93
|
+
return body;
|
|
94
|
+
},
|
|
95
|
+
async delete(key) {
|
|
96
|
+
const { client, bucket } = driverState;
|
|
97
|
+
requireBucket(bucket);
|
|
98
|
+
await client.send(new import_client_s3.DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
99
|
+
},
|
|
100
|
+
async signedUrl(key, expiresInSec) {
|
|
101
|
+
const { client, bucket } = driverState;
|
|
102
|
+
requireBucket(bucket);
|
|
103
|
+
return (0, import_s3_request_presigner.getSignedUrl)(client, new import_client_s3.GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function requireBucket(bucket) {
|
|
108
|
+
if (!bucket) {
|
|
109
|
+
throw new Error("@crowi/plugin-storage-aws-s3: bucket is not configured.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
113
|
+
0 && (module.exports = {
|
|
114
|
+
createS3Driver
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Readable } from 'node:stream';\nimport { z } from 'zod/v3';\nimport { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport type { AwsConfig } from '@crowi/plugin-aws';\nimport type { CrowiPlugin, PluginContext, StorageDriver } from '@crowi/plugin-api';\n\nconst S3StorageConfigSchema = z\n .object({\n bucket: z.string().default(''),\n })\n .strict();\n\ntype S3StorageConfig = z.infer<typeof S3StorageConfigSchema>;\n\nexport interface S3DriverState {\n client: S3Client;\n bucket: string;\n}\n\n/**\n * Module-scope state ref. `registerStorage` initialises it from the\n * boot-time config; the driver methods snapshot from it on every call;\n * `reconfigure` mutates its fields in place when admin saves new\n * values. The single-instance assumption is fine — the plugin\n * registers exactly one `'s3'` driver, owned by this module.\n */\nconst state: S3DriverState = {\n client: new S3Client({}),\n bucket: '',\n};\n\nconst plugin: CrowiPlugin = {\n name: '@crowi/plugin-storage-aws-s3',\n version: '0.1.0-dev',\n requires: ['@crowi/plugin-aws'],\n configSchema: S3StorageConfigSchema,\n adminPlacement: {\n label: 'AWS S3',\n icon: 'cloud',\n // section omitted: derived from registerStorage → 'storage'\n },\n\n registerStorage: (registry, ctx) => {\n applyConfigToState(ctx, state);\n registry.register('s3', createS3Driver(state));\n ctx.log.debug('registered s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n\n reconfigure: (ctx) => {\n applyConfigToState(ctx, state);\n ctx.log.debug('reconfigured s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n};\n\nexport default plugin;\n\nfunction applyConfigToState(ctx: PluginContext, target: S3DriverState): void {\n const own = ctx.config<S3StorageConfig>();\n const aws = ctx.dependencyConfig<AwsConfig>('@crowi/plugin-aws');\n target.client = new S3Client({\n region: aws.region || undefined,\n credentials:\n aws.accessKeyId && aws.secretAccessKey\n ? {\n accessKeyId: aws.accessKeyId,\n secretAccessKey: aws.secretAccessKey,\n }\n : undefined,\n });\n target.bucket = own.bucket;\n}\n\n/**\n * Build the storage driver. Methods read `driverState` *once at the\n * top* — a snapshot — so a `reconfigure` running concurrently with an\n * inflight `put` / `get` cannot swap the client mid-call. The next\n * call sees the new client/bucket.\n */\nexport function createS3Driver(driverState: S3DriverState): StorageDriver {\n return {\n async put(key, body, meta) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body as Buffer | Readable,\n ContentType: meta.contentType,\n }),\n );\n return { key };\n },\n\n async get(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));\n const body = response.Body;\n if (!body) {\n throw new Error(`S3 returned empty body for key '${key}'`);\n }\n return body as Readable;\n },\n\n async delete(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n },\n\n async signedUrl(key, expiresInSec) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n return getSignedUrl(client, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });\n },\n };\n}\n\nfunction requireBucket(bucket: string): void {\n if (!bucket) {\n throw new Error('@crowi/plugin-storage-aws-s3: bucket is not configured.');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,gBAAkB;AAClB,uBAAkF;AAClF,kCAA6B;AAI7B,IAAM,wBAAwB,YAC3B,OAAO;AAAA,EACN,QAAQ,YAAE,OAAO,EAAE,QAAQ,EAAE;AAC/B,CAAC,EACA,OAAO;AAgBV,IAAM,QAAuB;AAAA,EAC3B,QAAQ,IAAI,0BAAS,CAAC,CAAC;AAAA,EACvB,QAAQ;AACV;AAEA,IAAM,SAAsB;AAAA,EAC1B,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU,CAAC,mBAAmB;AAAA,EAC9B,cAAc;AAAA,EACd,gBAAgB;AAAA,IACd,OAAO;AAAA,IACP,MAAM;AAAA;AAAA,EAER;AAAA,EAEA,iBAAiB,CAAC,UAAU,QAAQ;AAClC,uBAAmB,KAAK,KAAK;AAC7B,aAAS,SAAS,MAAM,eAAe,KAAK,CAAC;AAC7C,QAAI,IAAI,MAAM,4CAA4C,MAAM,UAAU,SAAS;AAAA,EACrF;AAAA,EAEA,aAAa,CAAC,QAAQ;AACpB,uBAAmB,KAAK,KAAK;AAC7B,QAAI,IAAI,MAAM,8CAA8C,MAAM,UAAU,SAAS;AAAA,EACvF;AACF;AAEA,IAAO,gBAAQ;AAEf,SAAS,mBAAmB,KAAoB,QAA6B;AAC3E,QAAM,MAAM,IAAI,OAAwB;AACxC,QAAM,MAAM,IAAI,iBAA4B,mBAAmB;AAC/D,SAAO,SAAS,IAAI,0BAAS;AAAA,IAC3B,QAAQ,IAAI,UAAU;AAAA,IACtB,aACE,IAAI,eAAe,IAAI,kBACnB;AAAA,MACE,aAAa,IAAI;AAAA,MACjB,iBAAiB,IAAI;AAAA,IACvB,IACA;AAAA,EACR,CAAC;AACD,SAAO,SAAS,IAAI;AACtB;AAQO,SAAS,eAAe,aAA2C;AACxE,SAAO;AAAA,IACL,MAAM,IAAI,KAAK,MAAM,MAAM;AACzB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO;AAAA,QACX,IAAI,kCAAiB;AAAA,UACnB,QAAQ;AAAA,UACR,KAAK;AAAA,UACL,MAAM;AAAA,UACN,aAAa,KAAK;AAAA,QACpB,CAAC;AAAA,MACH;AACA,aAAO,EAAE,IAAI;AAAA,IACf;AAAA,IAEA,MAAM,IAAI,KAAK;AACb,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,WAAW,MAAM,OAAO,KAAK,IAAI,kCAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AACrF,YAAM,OAAO,SAAS;AACtB,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,mCAAmC,GAAG,GAAG;AAAA,MAC3D;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,KAAK;AAChB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO,KAAK,IAAI,qCAAoB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AAAA,IACzE;AAAA,IAEA,MAAM,UAAU,KAAK,cAAc;AACjC,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,iBAAO,0CAAa,QAAQ,IAAI,kCAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,WAAW,aAAa,CAAC;AAAA,IAC7G;AAAA,EACF;AACF;AAEA,SAAS,cAAc,QAAsB;AAC3C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { z } from "zod/v3";
|
|
3
|
+
import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
4
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
5
|
+
var S3StorageConfigSchema = z.object({
|
|
6
|
+
bucket: z.string().default("")
|
|
7
|
+
}).strict();
|
|
8
|
+
var state = {
|
|
9
|
+
client: new S3Client({}),
|
|
10
|
+
bucket: ""
|
|
11
|
+
};
|
|
12
|
+
var plugin = {
|
|
13
|
+
name: "@crowi/plugin-storage-aws-s3",
|
|
14
|
+
version: "0.1.0-dev",
|
|
15
|
+
requires: ["@crowi/plugin-aws"],
|
|
16
|
+
configSchema: S3StorageConfigSchema,
|
|
17
|
+
adminPlacement: {
|
|
18
|
+
label: "AWS S3",
|
|
19
|
+
icon: "cloud"
|
|
20
|
+
// section omitted: derived from registerStorage → 'storage'
|
|
21
|
+
},
|
|
22
|
+
registerStorage: (registry, ctx) => {
|
|
23
|
+
applyConfigToState(ctx, state);
|
|
24
|
+
registry.register("s3", createS3Driver(state));
|
|
25
|
+
ctx.log.debug("registered s3 storage driver (bucket=%s)", state.bucket || "<unset>");
|
|
26
|
+
},
|
|
27
|
+
reconfigure: (ctx) => {
|
|
28
|
+
applyConfigToState(ctx, state);
|
|
29
|
+
ctx.log.debug("reconfigured s3 storage driver (bucket=%s)", state.bucket || "<unset>");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var index_default = plugin;
|
|
33
|
+
function applyConfigToState(ctx, target) {
|
|
34
|
+
const own = ctx.config();
|
|
35
|
+
const aws = ctx.dependencyConfig("@crowi/plugin-aws");
|
|
36
|
+
target.client = new S3Client({
|
|
37
|
+
region: aws.region || void 0,
|
|
38
|
+
credentials: aws.accessKeyId && aws.secretAccessKey ? {
|
|
39
|
+
accessKeyId: aws.accessKeyId,
|
|
40
|
+
secretAccessKey: aws.secretAccessKey
|
|
41
|
+
} : void 0
|
|
42
|
+
});
|
|
43
|
+
target.bucket = own.bucket;
|
|
44
|
+
}
|
|
45
|
+
function createS3Driver(driverState) {
|
|
46
|
+
return {
|
|
47
|
+
async put(key, body, meta) {
|
|
48
|
+
const { client, bucket } = driverState;
|
|
49
|
+
requireBucket(bucket);
|
|
50
|
+
await client.send(
|
|
51
|
+
new PutObjectCommand({
|
|
52
|
+
Bucket: bucket,
|
|
53
|
+
Key: key,
|
|
54
|
+
Body: body,
|
|
55
|
+
ContentType: meta.contentType
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
return { key };
|
|
59
|
+
},
|
|
60
|
+
async get(key) {
|
|
61
|
+
const { client, bucket } = driverState;
|
|
62
|
+
requireBucket(bucket);
|
|
63
|
+
const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
|
|
64
|
+
const body = response.Body;
|
|
65
|
+
if (!body) {
|
|
66
|
+
throw new Error(`S3 returned empty body for key '${key}'`);
|
|
67
|
+
}
|
|
68
|
+
return body;
|
|
69
|
+
},
|
|
70
|
+
async delete(key) {
|
|
71
|
+
const { client, bucket } = driverState;
|
|
72
|
+
requireBucket(bucket);
|
|
73
|
+
await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
74
|
+
},
|
|
75
|
+
async signedUrl(key, expiresInSec) {
|
|
76
|
+
const { client, bucket } = driverState;
|
|
77
|
+
requireBucket(bucket);
|
|
78
|
+
return getSignedUrl(client, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function requireBucket(bucket) {
|
|
83
|
+
if (!bucket) {
|
|
84
|
+
throw new Error("@crowi/plugin-storage-aws-s3: bucket is not configured.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
createS3Driver,
|
|
89
|
+
index_default as default
|
|
90
|
+
};
|
|
91
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { Readable } from 'node:stream';\nimport { z } from 'zod/v3';\nimport { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport type { AwsConfig } from '@crowi/plugin-aws';\nimport type { CrowiPlugin, PluginContext, StorageDriver } from '@crowi/plugin-api';\n\nconst S3StorageConfigSchema = z\n .object({\n bucket: z.string().default(''),\n })\n .strict();\n\ntype S3StorageConfig = z.infer<typeof S3StorageConfigSchema>;\n\nexport interface S3DriverState {\n client: S3Client;\n bucket: string;\n}\n\n/**\n * Module-scope state ref. `registerStorage` initialises it from the\n * boot-time config; the driver methods snapshot from it on every call;\n * `reconfigure` mutates its fields in place when admin saves new\n * values. The single-instance assumption is fine — the plugin\n * registers exactly one `'s3'` driver, owned by this module.\n */\nconst state: S3DriverState = {\n client: new S3Client({}),\n bucket: '',\n};\n\nconst plugin: CrowiPlugin = {\n name: '@crowi/plugin-storage-aws-s3',\n version: '0.1.0-dev',\n requires: ['@crowi/plugin-aws'],\n configSchema: S3StorageConfigSchema,\n adminPlacement: {\n label: 'AWS S3',\n icon: 'cloud',\n // section omitted: derived from registerStorage → 'storage'\n },\n\n registerStorage: (registry, ctx) => {\n applyConfigToState(ctx, state);\n registry.register('s3', createS3Driver(state));\n ctx.log.debug('registered s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n\n reconfigure: (ctx) => {\n applyConfigToState(ctx, state);\n ctx.log.debug('reconfigured s3 storage driver (bucket=%s)', state.bucket || '<unset>');\n },\n};\n\nexport default plugin;\n\nfunction applyConfigToState(ctx: PluginContext, target: S3DriverState): void {\n const own = ctx.config<S3StorageConfig>();\n const aws = ctx.dependencyConfig<AwsConfig>('@crowi/plugin-aws');\n target.client = new S3Client({\n region: aws.region || undefined,\n credentials:\n aws.accessKeyId && aws.secretAccessKey\n ? {\n accessKeyId: aws.accessKeyId,\n secretAccessKey: aws.secretAccessKey,\n }\n : undefined,\n });\n target.bucket = own.bucket;\n}\n\n/**\n * Build the storage driver. Methods read `driverState` *once at the\n * top* — a snapshot — so a `reconfigure` running concurrently with an\n * inflight `put` / `get` cannot swap the client mid-call. The next\n * call sees the new client/bucket.\n */\nexport function createS3Driver(driverState: S3DriverState): StorageDriver {\n return {\n async put(key, body, meta) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body as Buffer | Readable,\n ContentType: meta.contentType,\n }),\n );\n return { key };\n },\n\n async get(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));\n const body = response.Body;\n if (!body) {\n throw new Error(`S3 returned empty body for key '${key}'`);\n }\n return body as Readable;\n },\n\n async delete(key) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n await client.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n },\n\n async signedUrl(key, expiresInSec) {\n const { client, bucket } = driverState;\n requireBucket(bucket);\n return getSignedUrl(client, new GetObjectCommand({ Bucket: bucket, Key: key }), { expiresIn: expiresInSec });\n },\n };\n}\n\nfunction requireBucket(bucket: string): void {\n if (!bucket) {\n throw new Error('@crowi/plugin-storage-aws-s3: bucket is not configured.');\n }\n}\n"],"mappings":";AACA,SAAS,SAAS;AAClB,SAAS,qBAAqB,kBAAkB,kBAAkB,gBAAgB;AAClF,SAAS,oBAAoB;AAI7B,IAAM,wBAAwB,EAC3B,OAAO;AAAA,EACN,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE;AAC/B,CAAC,EACA,OAAO;AAgBV,IAAM,QAAuB;AAAA,EAC3B,QAAQ,IAAI,SAAS,CAAC,CAAC;AAAA,EACvB,QAAQ;AACV;AAEA,IAAM,SAAsB;AAAA,EAC1B,MAAM;AAAA,EACN,SAAS;AAAA,EACT,UAAU,CAAC,mBAAmB;AAAA,EAC9B,cAAc;AAAA,EACd,gBAAgB;AAAA,IACd,OAAO;AAAA,IACP,MAAM;AAAA;AAAA,EAER;AAAA,EAEA,iBAAiB,CAAC,UAAU,QAAQ;AAClC,uBAAmB,KAAK,KAAK;AAC7B,aAAS,SAAS,MAAM,eAAe,KAAK,CAAC;AAC7C,QAAI,IAAI,MAAM,4CAA4C,MAAM,UAAU,SAAS;AAAA,EACrF;AAAA,EAEA,aAAa,CAAC,QAAQ;AACpB,uBAAmB,KAAK,KAAK;AAC7B,QAAI,IAAI,MAAM,8CAA8C,MAAM,UAAU,SAAS;AAAA,EACvF;AACF;AAEA,IAAO,gBAAQ;AAEf,SAAS,mBAAmB,KAAoB,QAA6B;AAC3E,QAAM,MAAM,IAAI,OAAwB;AACxC,QAAM,MAAM,IAAI,iBAA4B,mBAAmB;AAC/D,SAAO,SAAS,IAAI,SAAS;AAAA,IAC3B,QAAQ,IAAI,UAAU;AAAA,IACtB,aACE,IAAI,eAAe,IAAI,kBACnB;AAAA,MACE,aAAa,IAAI;AAAA,MACjB,iBAAiB,IAAI;AAAA,IACvB,IACA;AAAA,EACR,CAAC;AACD,SAAO,SAAS,IAAI;AACtB;AAQO,SAAS,eAAe,aAA2C;AACxE,SAAO;AAAA,IACL,MAAM,IAAI,KAAK,MAAM,MAAM;AACzB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO;AAAA,QACX,IAAI,iBAAiB;AAAA,UACnB,QAAQ;AAAA,UACR,KAAK;AAAA,UACL,MAAM;AAAA,UACN,aAAa,KAAK;AAAA,QACpB,CAAC;AAAA,MACH;AACA,aAAO,EAAE,IAAI;AAAA,IACf;AAAA,IAEA,MAAM,IAAI,KAAK;AACb,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,WAAW,MAAM,OAAO,KAAK,IAAI,iBAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AACrF,YAAM,OAAO,SAAS;AACtB,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,MAAM,mCAAmC,GAAG,GAAG;AAAA,MAC3D;AACA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,KAAK;AAChB,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,YAAM,OAAO,KAAK,IAAI,oBAAoB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC;AAAA,IACzE;AAAA,IAEA,MAAM,UAAU,KAAK,cAAc;AACjC,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,oBAAc,MAAM;AACpB,aAAO,aAAa,QAAQ,IAAI,iBAAiB,EAAE,QAAQ,QAAQ,KAAK,IAAI,CAAC,GAAG,EAAE,WAAW,aAAa,CAAC;AAAA,IAC7G;AAAA,EACF;AACF;AAEA,SAAS,cAAc,QAAsB;AAC3C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,yDAAyD;AAAA,EAC3E;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crowi/plugin-storage-aws-s3",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "AWS S3 storage driver for Crowi 2.0. Depends on @crowi/plugin-aws for shared credentials.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"zod": "^4.4.3"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@aws-sdk/client-s3": "^3.1060.0",
|
|
27
|
+
"@aws-sdk/s3-request-presigner": "^3.1060.0",
|
|
28
|
+
"@crowi/plugin-api": "^0.1.0-alpha.0",
|
|
29
|
+
"@crowi/plugin-aws": "^0.1.0-alpha.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^24",
|
|
33
|
+
"tsup": "^8.3.5",
|
|
34
|
+
"typescript": "^5.8.3",
|
|
35
|
+
"zod": "^4.4.3",
|
|
36
|
+
"@crowi/plugin-aws": "0.1.0-alpha.0",
|
|
37
|
+
"@crowi/tsconfig": "0.1.0-alpha.0",
|
|
38
|
+
"@crowi/plugin-api": "0.1.0-alpha.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsup --watch --no-clean",
|
|
43
|
+
"type-check": "tsc --noEmit"
|
|
44
|
+
}
|
|
45
|
+
}
|