diff --git a/data/imagegen.sqlite b/data/imagegen.sqlite index ee3950e..f0f8e83 100644 Binary files a/data/imagegen.sqlite and b/data/imagegen.sqlite differ diff --git a/data/imagegen.sqlite-shm b/data/imagegen.sqlite-shm index e24f6ce..e4a96c2 100644 Binary files a/data/imagegen.sqlite-shm and b/data/imagegen.sqlite-shm differ diff --git a/data/imagegen.sqlite-wal b/data/imagegen.sqlite-wal index 4d7cc4e..a0cfc12 100644 Binary files a/data/imagegen.sqlite-wal and b/data/imagegen.sqlite-wal differ diff --git a/package.json b/package.json index d6e82ba..75afecc 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1075.0", "@radix-ui/react-label": "^2.1.10", "@radix-ui/react-select": "^2.3.1", "@radix-ui/react-separator": "^1.1.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0066f0f..e7496d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1075.0 + version: 3.1075.0 '@radix-ui/react-label': specifier: ^2.1.10 version: 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -94,6 +97,109 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/checksums@3.1000.8': + resolution: {integrity: sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1075.0': + resolution: {integrity: sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.23': + resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.49': + resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.51': + resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.56': + resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.55': + resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.58': + resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.49': + resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.55': + resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.55': + resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.33': + resolution: {integrity: sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.54': + resolution: {integrity: sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.23': + resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1074.0': + resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.31': + resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -851,6 +957,42 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@smithy/core@3.26.0': + resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.2': + resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.5.2': + resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.8.2': + resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.2': + resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -1141,6 +1283,9 @@ packages: bl@2.2.0: resolution: {integrity: sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.15: resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} @@ -2634,6 +2779,235 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/checksums@3.1000.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1075.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/middleware-flexible-checksums': 3.974.33 + '@aws-sdk/middleware-sdk-s3': 3.972.54 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.23': + dependencies: + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.31 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.26.0 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.51': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.56': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-login': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.58': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-ini': 3.972.56 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/token-providers': 3.1074.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.33': + dependencies: + '@aws-sdk/checksums': 3.1000.8 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.23': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.35': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1074.0': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.31': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -3327,6 +3701,54 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@smithy/core@3.26.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -3643,6 +4065,8 @@ snapshots: readable-stream: 2.3.8 safe-buffer: 5.2.1 + bowser@2.14.1: {} + brace-expansion@1.1.15: dependencies: balanced-match: 1.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e6bcef7..84b784b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,7 @@ allowBuilds: better-sqlite3: true sharp: true + +onlyBuiltDependencies: + - better-sqlite3 + - sharp diff --git a/public/generated/1076263b-f569-46ad-8def-cdab26e83782.png b/public/generated/1076263b-f569-46ad-8def-cdab26e83782.png deleted file mode 100644 index 0efc7ef..0000000 Binary files a/public/generated/1076263b-f569-46ad-8def-cdab26e83782.png and /dev/null differ diff --git a/public/generated/2c0d3b04-f2cb-4285-9b17-207e7b1f46cd.png b/public/generated/2c0d3b04-f2cb-4285-9b17-207e7b1f46cd.png deleted file mode 100644 index 10f05c4..0000000 Binary files a/public/generated/2c0d3b04-f2cb-4285-9b17-207e7b1f46cd.png and /dev/null differ diff --git a/public/generated/5cd03fab-0d52-4dfd-be19-7b2d3a2abd1d.png b/public/generated/5cd03fab-0d52-4dfd-be19-7b2d3a2abd1d.png deleted file mode 100644 index eaf5ed3..0000000 Binary files a/public/generated/5cd03fab-0d52-4dfd-be19-7b2d3a2abd1d.png and /dev/null differ diff --git a/public/generated/5fcf60c6-8891-4698-a321-e1c84cde7424.png b/public/generated/5fcf60c6-8891-4698-a321-e1c84cde7424.png deleted file mode 100644 index 5e75d40..0000000 Binary files a/public/generated/5fcf60c6-8891-4698-a321-e1c84cde7424.png and /dev/null differ diff --git a/public/generated/6240b5e9-5fad-46a2-ae2e-9fe33500c07b.png b/public/generated/6240b5e9-5fad-46a2-ae2e-9fe33500c07b.png deleted file mode 100644 index d8c1b24..0000000 Binary files a/public/generated/6240b5e9-5fad-46a2-ae2e-9fe33500c07b.png and /dev/null differ diff --git a/public/generated/72944036-4c3d-463e-a56d-fe319ca41758.png b/public/generated/72944036-4c3d-463e-a56d-fe319ca41758.png deleted file mode 100644 index d552612..0000000 Binary files a/public/generated/72944036-4c3d-463e-a56d-fe319ca41758.png and /dev/null differ diff --git a/public/generated/867c0cec-e5b0-4050-addb-1e78c57709e3.png b/public/generated/867c0cec-e5b0-4050-addb-1e78c57709e3.png deleted file mode 100644 index d090e55..0000000 Binary files a/public/generated/867c0cec-e5b0-4050-addb-1e78c57709e3.png and /dev/null differ diff --git a/public/generated/986eba8a-3c51-478f-9443-79e2c47da065.png b/public/generated/986eba8a-3c51-478f-9443-79e2c47da065.png deleted file mode 100644 index 0efc7ef..0000000 Binary files a/public/generated/986eba8a-3c51-478f-9443-79e2c47da065.png and /dev/null differ diff --git a/public/generated/9f78c806-8b98-4f34-b188-a797053cb3f4.png b/public/generated/9f78c806-8b98-4f34-b188-a797053cb3f4.png deleted file mode 100644 index 1cb1ca4..0000000 Binary files a/public/generated/9f78c806-8b98-4f34-b188-a797053cb3f4.png and /dev/null differ diff --git a/public/generated/aaad7aac-ec4d-4746-9e8e-77842716dc9b.png b/public/generated/aaad7aac-ec4d-4746-9e8e-77842716dc9b.png deleted file mode 100644 index 10f05c4..0000000 Binary files a/public/generated/aaad7aac-ec4d-4746-9e8e-77842716dc9b.png and /dev/null differ diff --git a/public/generated/cd311e44-1271-4452-9be3-0ddaa0ad01d6.png b/public/generated/cd311e44-1271-4452-9be3-0ddaa0ad01d6.png deleted file mode 100644 index 93bfb13..0000000 Binary files a/public/generated/cd311e44-1271-4452-9be3-0ddaa0ad01d6.png and /dev/null differ diff --git a/public/generated/dfd90edd-8a6b-4ae6-93df-781cea2dfa2f.png b/public/generated/dfd90edd-8a6b-4ae6-93df-781cea2dfa2f.png deleted file mode 100644 index 3053217..0000000 Binary files a/public/generated/dfd90edd-8a6b-4ae6-93df-781cea2dfa2f.png and /dev/null differ diff --git a/public/generated/e3c036a2-c311-4be0-8970-d4b4b14dac7f.png b/public/generated/e3c036a2-c311-4be0-8970-d4b4b14dac7f.png deleted file mode 100644 index 3053217..0000000 Binary files a/public/generated/e3c036a2-c311-4be0-8970-d4b4b14dac7f.png and /dev/null differ diff --git a/public/generated/ea78b81b-8487-4061-bef6-0689554b6c5a.png b/public/generated/ea78b81b-8487-4061-bef6-0689554b6c5a.png deleted file mode 100644 index d090e55..0000000 Binary files a/public/generated/ea78b81b-8487-4061-bef6-0689554b6c5a.png and /dev/null differ diff --git a/public/generated/ee6f6b18-3c7b-4399-8032-f01035c05c91.png b/public/generated/ee6f6b18-3c7b-4399-8032-f01035c05c91.png deleted file mode 100644 index 8d375e9..0000000 Binary files a/public/generated/ee6f6b18-3c7b-4399-8032-f01035c05c91.png and /dev/null differ diff --git a/src/app/api/canvas/projects/[id]/export/route.ts b/src/app/api/canvas/projects/[id]/export/route.ts index d71fd03..c152d1e 100644 --- a/src/app/api/canvas/projects/[id]/export/route.ts +++ b/src/app/api/canvas/projects/[id]/export/route.ts @@ -20,7 +20,7 @@ export async function GET( try { return NextResponse.json({ - item: createCanvasProjectExport({ + item: await createCanvasProjectExport({ title: project.title, description: project.description, data: project.data, diff --git a/src/app/api/canvas/projects/import/route.ts b/src/app/api/canvas/projects/import/route.ts index 2b63d0a..7b90f48 100644 --- a/src/app/api/canvas/projects/import/route.ts +++ b/src/app/api/canvas/projects/import/route.ts @@ -7,7 +7,7 @@ export async function POST(request: Request) { const payload = await request.json().catch(() => null); try { - const project = importCanvasProjectExport(payload); + const project = await importCanvasProjectExport(payload); return NextResponse.json({ item: project }, { status: 201 }); } catch (err) { return NextResponse.json( diff --git a/src/app/api/canvas/uploads/route.ts b/src/app/api/canvas/uploads/route.ts index 1c7048e..9e1989d 100644 --- a/src/app/api/canvas/uploads/route.ts +++ b/src/app/api/canvas/uploads/route.ts @@ -13,7 +13,7 @@ export async function POST(request: Request) { const buffer = Buffer.from(await image.arrayBuffer()); const id = crypto.randomUUID(); - const persisted = persistUploadedImageFile(id, image.name || "canvas-upload.png", buffer); + const persisted = await persistUploadedImageFile(id, image.name || "canvas-upload.png", buffer); return NextResponse.json({ item: { diff --git a/src/components/direct/direct-studio.tsx b/src/components/direct/direct-studio.tsx index 5d151f0..c39bbf0 100644 --- a/src/components/direct/direct-studio.tsx +++ b/src/components/direct/direct-studio.tsx @@ -609,29 +609,30 @@ export function DirectStudio({ activeView }: { activeView: ActiveView }) { ) : history.length ? (
{history.map((item) => ( - - +
))} ) : ( @@ -643,35 +644,36 @@ export function DirectStudio({ activeView }: { activeView: ActiveView }) {
{history.length ? ( history.map((item) => ( - - +
)) ) : (
暂无历史记录
diff --git a/src/lib/server/services/canvas-project-transfer.ts b/src/lib/server/services/canvas-project-transfer.ts index 741ba9e..b262ac7 100644 --- a/src/lib/server/services/canvas-project-transfer.ts +++ b/src/lib/server/services/canvas-project-transfer.ts @@ -1,4 +1,3 @@ -import { existsSync, readFileSync } from "node:fs"; import { extname, basename } from "node:path"; import type { CanvasImageNodeMetadata, CanvasProjectData } from "@/lib/canvas/types"; import { normalizeCanvasProjectData } from "@/lib/canvas/types"; @@ -6,6 +5,7 @@ import { createCanvasProject } from "@/lib/server/repositories/canvas-project-re import { getGeneratedImagePathFromUrl, persistImageBuffer, + readPersistedImageFile, } from "@/lib/server/storage/image-file-storage"; const CANVAS_EXPORT_KIND = "imagegen.canvas.project"; @@ -29,11 +29,11 @@ export type CanvasProjectExportV1 = { assets: CanvasProjectExportAsset[]; }; -export function createCanvasProjectExport(input: { +export async function createCanvasProjectExport(input: { title: string; description?: string; data: CanvasProjectData; -}): CanvasProjectExportV1 { +}): Promise { return { kind: CANVAS_EXPORT_KIND, version: CANVAS_EXPORT_VERSION, @@ -41,11 +41,11 @@ export function createCanvasProjectExport(input: { title: input.title, description: input.description ?? "", data: input.data, - assets: collectExportAssets(input.data), + assets: await collectExportAssets(input.data), }; } -export function importCanvasProjectExport(value: unknown) { +export async function importCanvasProjectExport(value: unknown) { const payload = normalizeCanvasProjectExport(value); if (!payload) { throw new Error("导入 JSON 不是有效的画布导出格式"); @@ -54,12 +54,12 @@ export function importCanvasProjectExport(value: unknown) { const assetsByNodeId = new Map(payload.assets.map((asset) => [asset.nodeId, asset])); const data: CanvasProjectData = { ...payload.data, - nodes: payload.data.nodes.map((node) => { + nodes: await Promise.all(payload.data.nodes.map(async (node) => { if (node.type !== "image") return node; const asset = assetsByNodeId.get(node.id); if (!asset) return node; - const persisted = persistDataUrl(asset.dataUrl, asset.fileName); + const persisted = await persistDataUrl(asset.dataUrl, asset.fileName); return { ...node, metadata: { @@ -68,7 +68,7 @@ export function importCanvasProjectExport(value: unknown) { filePath: persisted.filePath, } satisfies CanvasImageNodeMetadata, }; - }), + })), }; const title = payload.title.trim() @@ -82,7 +82,7 @@ export function importCanvasProjectExport(value: unknown) { }); } -function collectExportAssets(data: CanvasProjectData) { +async function collectExportAssets(data: CanvasProjectData) { const assets: CanvasProjectExportAsset[] = []; for (const node of data.nodes) { @@ -91,11 +91,15 @@ function collectExportAssets(data: CanvasProjectData) { if (!metadata.imageUrl) continue; const filePath = metadata.filePath ?? getGeneratedImagePathFromUrl(metadata.imageUrl); - if (!filePath || !existsSync(filePath)) { + if (!filePath) { + throw new Error(`图片文件不存在:${node.title || node.id}`); + } + + const buffer = await readPersistedImageFile(filePath); + if (!buffer) { throw new Error(`图片文件不存在:${node.title || node.id}`); } - const buffer = readFileSync(filePath); const mimeType = getMimeType(filePath); assets.push({ nodeId: node.id, diff --git a/src/lib/server/services/image-job-service.ts b/src/lib/server/services/image-job-service.ts index babc87c..62ca4fa 100644 --- a/src/lib/server/services/image-job-service.ts +++ b/src/lib/server/services/image-job-service.ts @@ -455,7 +455,7 @@ async function runImageJob( }); const id = crypto.randomUUID(); - const persisted = persistBase64Image(id, input.outputFormat, b64); + const persisted = await persistBase64Image(id, input.outputFormat, b64); const createdAt = new Date().toISOString(); const resultPayload = { id, diff --git a/src/lib/server/storage/image-file-storage.ts b/src/lib/server/storage/image-file-storage.ts index e4d88e9..190b4fe 100644 --- a/src/lib/server/storage/image-file-storage.ts +++ b/src/lib/server/storage/image-file-storage.ts @@ -1,5 +1,7 @@ +import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { basename, join } from "node:path"; +import { Readable } from "node:stream"; const imageDir = join( /* turbopackIgnore: true */ process.cwd(), @@ -13,6 +15,23 @@ export type PersistedImageFile = { imageUrl: string; }; +type S3StorageConfig = { + endpoint: string; + accessKeyId: string; + secretAccessKey: string; + bucket: string; + region: string; + publicUrl?: string; +}; + +const s3ObjectPrefix = "generated"; +const defaultBucket = "imagegen-tools"; +const defaultRegion = "us-east-1"; + +const globalForS3Storage = globalThis as typeof globalThis & { + imageStorageS3Client?: S3Client; +}; + export function getImageDir() { if (!existsSync(imageDir)) { mkdirSync(imageDir, { recursive: true }); @@ -21,27 +40,54 @@ export function getImageDir() { return imageDir; } -export function persistBase64Image( +export async function persistBase64Image( id: string, extension: string, base64: string, -): PersistedImageFile { +): Promise { const normalizedExtension = extension === "jpeg" ? "jpg" : extension; const fileName = `${id}.${normalizedExtension}`; - const filePath = join(getImageDir(), fileName); - const imageUrl = `/generated/${fileName}`; - writeFileSync(filePath, Buffer.from(base64, "base64")); - - return { fileName, filePath, imageUrl }; + return persistImageBufferWithFileName(fileName, Buffer.from(base64, "base64")); } -export function persistUploadedImageFile( +export async function persistUploadedImageFile( id: string, filename: string, buffer: Buffer, -): PersistedImageFile { +): Promise { const extension = getExtensionFromFilename(filename); const fileName = `${id}.${extension}`; + return persistImageBufferWithFileName(fileName, buffer); +} + +export async function persistImageBuffer( + id: string, + extension: string, + buffer: Buffer, +): Promise { + const normalizedExtension = normalizeExtension(extension); + const fileName = `${id}.${normalizedExtension}`; + return persistImageBufferWithFileName(fileName, buffer); +} + +export async function readPersistedImageFile(filePath: string): Promise { + if (filePath.startsWith("s3://")) { + return readS3Object(filePath); + } + + const { readFileSync } = await import("node:fs"); + if (!existsSync(filePath)) return null; + return readFileSync(filePath); +} + +export function isLocalImageFile(filePath: string) { + return !filePath.startsWith("s3://"); +} + +function persistImageBufferLocally( + fileName: string, + buffer: Buffer, +): PersistedImageFile { const filePath = join(getImageDir(), fileName); const imageUrl = `/generated/${fileName}`; writeFileSync(filePath, buffer); @@ -49,18 +95,33 @@ export function persistUploadedImageFile( return { fileName, filePath, imageUrl }; } -export function persistImageBuffer( - id: string, - extension: string, +async function persistImageBufferWithFileName( + fileName: string, buffer: Buffer, -): PersistedImageFile { - const normalizedExtension = normalizeExtension(extension); - const fileName = `${id}.${normalizedExtension}`; - const filePath = join(getImageDir(), fileName); - const imageUrl = `/generated/${fileName}`; - writeFileSync(filePath, buffer); +): Promise { + const config = getS3StorageConfig(); + if (!config) { + return persistImageBufferLocally(fileName, buffer); + } - return { fileName, filePath, imageUrl }; + const key = `${s3ObjectPrefix}/${fileName}`; + const contentType = getMimeTypeFromFileName(fileName); + const client = getS3Client(config); + + await client.send( + new PutObjectCommand({ + Bucket: config.bucket, + Key: key, + Body: buffer, + ContentType: contentType, + }), + ); + + return { + fileName, + filePath: `s3://${config.bucket}/${key}`, + imageUrl: createPublicImageUrl(config, key), + }; } export function getGeneratedImagePathFromUrl(imageUrl: string) { @@ -68,6 +129,104 @@ export function getGeneratedImagePathFromUrl(imageUrl: string) { return join(getImageDir(), basename(imageUrl)); } +function getS3StorageConfig(): S3StorageConfig | null { + const endpoint = process.env.FILE_STORAGE_API?.trim(); + const accessKeyId = process.env.FILE_STORAGE_ACCESS_KEY?.trim(); + const secretAccessKey = process.env.FILE_STORAGE_SECRET_KEY?.trim(); + const bucket = process.env.FILE_STORAGE_BUCKET?.trim() || defaultBucket; + + // ✅ 添加调试日志 + console.log('S3 Config Check:', { + hasEndpoint: !!endpoint, + hasAccessKey: !!accessKeyId, + hasSecretKey: !!secretAccessKey, + bucket, + endpoint: endpoint ? endpoint.replace(/\/+$/, "") : 'not set', + }); + + if (!endpoint || !accessKeyId || !secretAccessKey) { + return null; + } + + return { + endpoint: endpoint.replace(/\/+$/, ""), + accessKeyId, + secretAccessKey, + bucket: process.env.FILE_STORAGE_BUCKET?.trim() || defaultBucket, + region: process.env.FILE_STORAGE_REGION?.trim() || defaultRegion, + publicUrl: process.env.FILE_STORAGE_PUBLIC_URL?.trim().replace(/\/+$/, ""), + }; +} + +function getS3Client(config: S3StorageConfig) { + if (!globalForS3Storage.imageStorageS3Client) { + globalForS3Storage.imageStorageS3Client = new S3Client({ + endpoint: config.endpoint, + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + forcePathStyle: true, + }); + } + + return globalForS3Storage.imageStorageS3Client; +} + +async function readS3Object(filePath: string) { + const config = getS3StorageConfig(); + if (!config) return null; + + const object = parseS3FilePath(filePath); + if (!object) return null; + + const response = await getS3Client(config).send( + new GetObjectCommand({ + Bucket: object.bucket, + Key: object.key, + }), + ); + + if (!response.Body) return null; + if (response.Body instanceof Readable) { + return streamToBuffer(response.Body); + } + + const body = response.Body as { transformToByteArray?: () => Promise }; + if (body.transformToByteArray) { + return Buffer.from(await body.transformToByteArray()); + } + + return null; +} + +function parseS3FilePath(filePath: string) { + const match = /^s3:\/\/([^/]+)\/(.+)$/.exec(filePath); + if (!match) return null; + return { + bucket: match[1], + key: match[2], + }; +} + +async function streamToBuffer(stream: Readable) { + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks); +} + +function createPublicImageUrl(config: S3StorageConfig, key: string) { + const encodedKey = key.split("/").map(encodeURIComponent).join("/"); + if (config.publicUrl) { + return `${config.publicUrl}/${encodedKey}`; + } + + return `${config.endpoint}/${config.bucket}/${encodedKey}`; +} + function getExtensionFromFilename(filename: string) { const parts = filename.split("."); const extension = parts.at(-1)?.toLowerCase(); @@ -78,3 +237,10 @@ function normalizeExtension(extension: string | undefined) { if (!extension || extension.length > 5) return "png"; return extension === "jpeg" ? "jpg" : extension; } + +function getMimeTypeFromFileName(fileName: string) { + const extension = getExtensionFromFilename(fileName); + if (extension === "jpg" || extension === "jpeg") return "image/jpeg"; + if (extension === "webp") return "image/webp"; + return "image/png"; +}