next-tinacms-s3 5.0.9 → 7.0.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.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
 
3
3
  */
4
- import { S3ClientConfig } from '@aws-sdk/client-s3';
4
+ import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3';
5
5
  import { NextApiRequest, NextApiResponse } from 'next';
6
6
  export interface S3Config {
7
7
  config: S3ClientConfig;
@@ -18,3 +18,4 @@ export declare const mediaHandlerConfig: {
18
18
  };
19
19
  };
20
20
  export declare const createMediaHandler: (config: S3Config, options?: S3Options) => (req: NextApiRequest, res: NextApiResponse) => Promise<void>;
21
+ export declare const getUploadUrl: (bucket: string, key: string, expiresIn: number, client: S3Client) => Promise<string>;
package/dist/handlers.js CHANGED
@@ -26,14 +26,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
26
26
  var handlers_exports = {};
27
27
  __export(handlers_exports, {
28
28
  createMediaHandler: () => createMediaHandler,
29
+ getUploadUrl: () => getUploadUrl,
29
30
  mediaHandlerConfig: () => mediaHandlerConfig
30
31
  });
31
32
  module.exports = __toCommonJS(handlers_exports);
32
33
  var import_client_s3 = require("@aws-sdk/client-s3");
34
+ var import_s3_request_presigner = require("@aws-sdk/s3-request-presigner");
33
35
  var import_path = __toESM(require("path"));
34
- var import_fs = __toESM(require("fs"));
35
- var import_multer = __toESM(require("multer"));
36
- var import_util = require("util");
37
36
  var mediaHandlerConfig = {
38
37
  api: {
39
38
  bodyParser: false
@@ -63,9 +62,25 @@ var createMediaHandler = (config, options) => {
63
62
  }
64
63
  switch (req.method) {
65
64
  case "GET":
66
- return listMedia(req, res, client, bucket, mediaRoot, cdnUrl);
67
- case "POST":
68
- return uploadMedia(req, res, client, bucket, mediaRoot, cdnUrl);
65
+ if (req.url.startsWith("/api/s3/media/upload_url")) {
66
+ const expiresIn = req.query.expiresIn && Number(req.query.expiresIn) || 3600;
67
+ const s3_key = req.query.key ? Array.isArray(req.query.key) ? req.query.key[0] : req.query.key : null;
68
+ if (!s3_key) {
69
+ return res.status(400).json({ message: "key is required" });
70
+ }
71
+ if (await keyExists(client, bucket, s3_key)) {
72
+ return res.status(400).json({ message: "key already exists" });
73
+ }
74
+ const signedUrl = await getUploadUrl(
75
+ bucket,
76
+ s3_key,
77
+ expiresIn,
78
+ client
79
+ );
80
+ return res.json({ signedUrl, src: cdnUrl + s3_key });
81
+ } else {
82
+ return listMedia(req, res, client, bucket, mediaRoot, cdnUrl);
83
+ }
69
84
  case "DELETE":
70
85
  return deleteAsset(req, res, client, bucket);
71
86
  default:
@@ -73,66 +88,6 @@ var createMediaHandler = (config, options) => {
73
88
  }
74
89
  };
75
90
  };
76
- async function uploadMedia(req, res, client, bucket, mediaRoot, cdnUrl) {
77
- var _a;
78
- try {
79
- const upload = (0, import_util.promisify)(
80
- (0, import_multer.default)({
81
- storage: import_multer.default.diskStorage({
82
- directory: (req2, file, cb) => {
83
- cb(null, "/tmp");
84
- },
85
- filename: (req2, file, cb) => {
86
- cb(null, file.originalname);
87
- }
88
- })
89
- }).single("file")
90
- );
91
- await upload(req, res);
92
- const { directory } = req.body;
93
- let prefix = directory.replace(/^\//, "").replace(/\/$/, "");
94
- if (prefix)
95
- prefix = prefix + "/";
96
- const filePath = req.file.path;
97
- const fileType = (_a = req.file) == null ? void 0 : _a.mimetype;
98
- const blob = import_fs.default.readFileSync(filePath);
99
- const filename = import_path.default.basename(filePath);
100
- const params = {
101
- Bucket: bucket,
102
- Key: mediaRoot ? import_path.default.join(mediaRoot, prefix + filename) : prefix + filename,
103
- Body: blob,
104
- ACL: "public-read",
105
- ContentType: fileType || "application/octet-stream"
106
- };
107
- const command = new import_client_s3.PutObjectCommand(params);
108
- try {
109
- await client.send(command);
110
- const src = cdnUrl + prefix + filename;
111
- res.json({
112
- type: "file",
113
- id: prefix + filename,
114
- filename,
115
- directory: prefix,
116
- thumbnails: {
117
- "75x75": src,
118
- "400x400": src,
119
- "1000x1000": src
120
- },
121
- src: cdnUrl + (mediaRoot ? import_path.default.join(mediaRoot, prefix + filename) : prefix + filename)
122
- });
123
- } catch (e) {
124
- console.error("Error uploading media to s3");
125
- console.error(e);
126
- res.status(500).send(findErrorMessage(e));
127
- }
128
- } catch (e) {
129
- console.error("Error uploading media");
130
- console.error(e);
131
- res.status(500);
132
- const message = findErrorMessage(e);
133
- res.json({ e: message });
134
- }
135
- }
136
91
  function stripMediaRoot(mediaRoot, key) {
137
92
  if (!mediaRoot) {
138
93
  return key;
@@ -228,6 +183,35 @@ async function deleteAsset(req, res, client, bucket) {
228
183
  res.json({ e: message });
229
184
  }
230
185
  }
186
+ async function keyExists(client, bucket, key) {
187
+ var _a, _b;
188
+ try {
189
+ const cmd = new import_client_s3.HeadObjectCommand({
190
+ Bucket: bucket,
191
+ Key: key
192
+ });
193
+ const output = await client.send(cmd);
194
+ return output && output.$metadata.httpStatusCode === 200;
195
+ } catch (error) {
196
+ if (((_a = error.$metadata) == null ? void 0 : _a.httpStatusCode) === 404) {
197
+ return false;
198
+ } else if (((_b = error.$metadata) == null ? void 0 : _b.httpStatusCode) === 403) {
199
+ return false;
200
+ } else {
201
+ throw new Error("unexpected error checking if key exists");
202
+ }
203
+ }
204
+ }
205
+ var getUploadUrl = async (bucket, key, expiresIn, client) => {
206
+ return (0, import_s3_request_presigner.getSignedUrl)(
207
+ client,
208
+ new import_client_s3.PutObjectCommand({
209
+ Bucket: bucket,
210
+ Key: key
211
+ }),
212
+ { expiresIn }
213
+ );
214
+ };
231
215
  function getS3ToTinaFunc(cdnUrl, mediaRoot) {
232
216
  return function s3ToTina(file) {
233
217
  const strippedKey = stripMediaRoot(mediaRoot, file.Key);
@@ -251,5 +235,6 @@ function getS3ToTinaFunc(cdnUrl, mediaRoot) {
251
235
  // Annotate the CommonJS export names for ESM import in node:
252
236
  0 && (module.exports = {
253
237
  createMediaHandler,
238
+ getUploadUrl,
254
239
  mediaHandlerConfig
255
240
  });
package/dist/index.js CHANGED
@@ -47,6 +47,7 @@
47
47
  return E_DEFAULT;
48
48
  }
49
49
  };
50
+ const s3ErrorRegex = /<Error>.*<Code>(.+)<\/Code>.*<Message>(.+)<\/Message>.*/;
50
51
  class S3MediaStore {
51
52
  constructor() {
52
53
  this.fetchFunction = (input, init) => {
@@ -60,24 +61,57 @@
60
61
  async persist(media) {
61
62
  const newFiles = [];
62
63
  for (const item of media) {
63
- const { file, directory } = item;
64
- const formData = new FormData();
65
- formData.append("file", file);
66
- formData.append("directory", directory);
67
- formData.append("filename", file.name);
68
- const res = await this.fetchFunction(`/api/s3/media`, {
69
- method: "POST",
70
- body: formData
71
- });
64
+ let directory = item.directory;
65
+ if (directory == null ? void 0 : directory.endsWith("/")) {
66
+ directory = directory.substr(0, directory.length - 1);
67
+ }
68
+ const path = `${directory && directory !== "/" ? `${directory}/${item.file.name}` : item.file.name}`;
69
+ const res = await this.fetchFunction(
70
+ `/api/s3/media/upload_url?key=${path}`,
71
+ {
72
+ method: "GET"
73
+ }
74
+ );
72
75
  if (res.status != 200) {
73
76
  const responseData = await res.json();
74
77
  throw new Error(responseData.message);
75
78
  }
76
- const fileRes = await res.json();
79
+ const { signedUrl, src } = await res.json();
80
+ if (!signedUrl || !src) {
81
+ throw new Error("Unexpected error generating upload url");
82
+ }
83
+ const uploadRes = await fetch(signedUrl, {
84
+ method: "PUT",
85
+ body: item.file,
86
+ headers: {
87
+ "Content-Type": item.file.type || "application/octet-stream"
88
+ }
89
+ });
90
+ if (!uploadRes.ok) {
91
+ const xmlRes = await uploadRes.text();
92
+ const matches = s3ErrorRegex.exec(xmlRes);
93
+ console.error(xmlRes);
94
+ if (!matches) {
95
+ throw new Error("Unexpected error uploading media asset");
96
+ } else {
97
+ throw new Error(`Upload error: '${matches[2]}'`);
98
+ }
99
+ }
77
100
  await new Promise((resolve) => {
78
101
  setTimeout(resolve, 2e3);
79
102
  });
80
- newFiles.push(fileRes);
103
+ newFiles.push({
104
+ directory: item.directory,
105
+ filename: item.file.name,
106
+ id: item.file.name,
107
+ type: "file",
108
+ thumbnails: {
109
+ "75x75": src,
110
+ "400x400": src,
111
+ "1000x1000": src
112
+ },
113
+ src
114
+ });
81
115
  }
82
116
  return newFiles;
83
117
  }
package/dist/index.mjs CHANGED
@@ -44,6 +44,7 @@ const interpretErrorMessage = (message) => {
44
44
  return E_DEFAULT;
45
45
  }
46
46
  };
47
+ const s3ErrorRegex = /<Error>.*<Code>(.+)<\/Code>.*<Message>(.+)<\/Message>.*/;
47
48
  class S3MediaStore {
48
49
  constructor() {
49
50
  this.fetchFunction = (input, init) => {
@@ -57,24 +58,57 @@ class S3MediaStore {
57
58
  async persist(media) {
58
59
  const newFiles = [];
59
60
  for (const item of media) {
60
- const { file, directory } = item;
61
- const formData = new FormData();
62
- formData.append("file", file);
63
- formData.append("directory", directory);
64
- formData.append("filename", file.name);
65
- const res = await this.fetchFunction(`/api/s3/media`, {
66
- method: "POST",
67
- body: formData
68
- });
61
+ let directory = item.directory;
62
+ if (directory == null ? void 0 : directory.endsWith("/")) {
63
+ directory = directory.substr(0, directory.length - 1);
64
+ }
65
+ const path = `${directory && directory !== "/" ? `${directory}/${item.file.name}` : item.file.name}`;
66
+ const res = await this.fetchFunction(
67
+ `/api/s3/media/upload_url?key=${path}`,
68
+ {
69
+ method: "GET"
70
+ }
71
+ );
69
72
  if (res.status != 200) {
70
73
  const responseData = await res.json();
71
74
  throw new Error(responseData.message);
72
75
  }
73
- const fileRes = await res.json();
76
+ const { signedUrl, src } = await res.json();
77
+ if (!signedUrl || !src) {
78
+ throw new Error("Unexpected error generating upload url");
79
+ }
80
+ const uploadRes = await fetch(signedUrl, {
81
+ method: "PUT",
82
+ body: item.file,
83
+ headers: {
84
+ "Content-Type": item.file.type || "application/octet-stream"
85
+ }
86
+ });
87
+ if (!uploadRes.ok) {
88
+ const xmlRes = await uploadRes.text();
89
+ const matches = s3ErrorRegex.exec(xmlRes);
90
+ console.error(xmlRes);
91
+ if (!matches) {
92
+ throw new Error("Unexpected error uploading media asset");
93
+ } else {
94
+ throw new Error(`Upload error: '${matches[2]}'`);
95
+ }
96
+ }
74
97
  await new Promise((resolve) => {
75
98
  setTimeout(resolve, 2e3);
76
99
  });
77
- newFiles.push(fileRes);
100
+ newFiles.push({
101
+ directory: item.directory,
102
+ filename: item.file.name,
103
+ id: item.file.name,
104
+ type: "file",
105
+ thumbnails: {
106
+ "75x75": src,
107
+ "400x400": src,
108
+ "1000x1000": src
109
+ },
110
+ src
111
+ });
78
112
  }
79
113
  return newFiles;
80
114
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-tinacms-s3",
3
- "version": "5.0.9",
3
+ "version": "7.0.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "files": [
@@ -26,11 +26,11 @@
26
26
  "react": "^18.3.1",
27
27
  "react-dom": "^18.3.1",
28
28
  "typescript": "^5.6.2",
29
- "@tinacms/scripts": "1.2.3",
30
- "tinacms": "2.2.9"
29
+ "@tinacms/scripts": "1.3.0",
30
+ "tinacms": "2.4.0"
31
31
  },
32
32
  "peerDependencies": {
33
- "tinacms": "2.2.9"
33
+ "tinacms": "2.4.0"
34
34
  },
35
35
  "publishConfig": {
36
36
  "registry": "https://registry.npmjs.org"
@@ -40,8 +40,8 @@
40
40
  "directory": "packages/next-tinacms-s3"
41
41
  },
42
42
  "dependencies": {
43
- "@aws-sdk/client-s3": "^3.658.1",
44
- "multer": "1.4.5-lts.1"
43
+ "@aws-sdk/s3-request-presigner": "^3.658.1",
44
+ "@aws-sdk/client-s3": "^3.658.1"
45
45
  },
46
46
  "scripts": {
47
47
  "types": "pnpm tsc",