Feature/google cloud storage (#316)

* environment setup and reproduce

* rmv my dashboard

* Stop tracking my-dashboard

* gitignore: ignore all .env files

* working UI, not yet tested

* Update credential validation

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* tested with fake-gcs-server

* fix: migration

* fix: migration, init for gcs and refactoring

* fix: get object gcs

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
This commit is contained in:
Hayzie Chu
2026-06-27 20:35:59 +02:00
committed by GitHub
co-authored by coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Charles GTE
parent 79832f0d0b
commit 8f44071069
17 changed files with 3656 additions and 14 deletions
+4
View File
@@ -9,6 +9,8 @@
# testing
/coverage
gcs-data/
gcs-storage/
# next.js
/.next/
@@ -28,6 +30,8 @@ yarn-error.log*
# local env files
.env*.local
.env
**/.env
# vercel
.vercel
+47
View File
@@ -55,9 +55,56 @@ services:
# DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;
gcs:
image: fsouza/fake-gcs-server:latest
container_name: fake-gcs
ports:
- "4443:4443"
command:
- "-scheme"
- "http"
- "-port"
- "4443"
- "-public-host"
- "localhost:4443"
- "-external-url"
- "http://localhost:4443"
- "-backend"
- "filesystem"
- "-filesystem-root"
- "/storage"
volumes:
- gcs-data:/data
- gcs-storage:/storage
networks:
- portabase
gcs-init:
image: curlimages/curl:latest
depends_on:
- gcs
entrypoint: ["/bin/sh", "-c"]
command: >
'until curl -fsS http://gcs:4443/storage/v1/b >/dev/null; do sleep 1; done;
curl -fsS http://gcs:4443/storage/v1/b/portabase >/dev/null
|| curl -fsS -X POST "http://gcs:4443/storage/v1/b?project=local"
-H "Content-Type: application/json"
-d "{\"name\":\"portabase\"}"'
networks:
- portabase
# curl http://localhost:4443/storage/v1/b
# projectId: "test-project",
# bucketName: "portabase",
# clientEmail: "fake@test.iam.gserviceaccount.com",
# privateKey: "unused-when-emulated",
volumes:
postgres-data:
azurite-data:
gcs-data:
gcs-storage:
networks:
portabase:
+1
View File
@@ -21,6 +21,7 @@
"@better-auth/core": "1.6.11",
"@better-auth/passkey": "1.6.11",
"@better-auth/sso": "1.6.11",
"@google-cloud/storage": "^7.19.0",
"@hookform/resolvers": "^5.4.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@onboardjs/react": "1.0.0-rc.5",
+285 -12
View File
@@ -26,6 +26,9 @@ importers:
'@better-auth/sso':
specifier: 1.6.11
version: 1.6.11(@better-auth/core@1.6.11(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.5(zod@4.3.6))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.0)(@better-fetch/fetch@1.1.21)(better-auth@1.6.11(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@prisma/client@6.7.0(prisma@6.7.0(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3)))(next@16.2.3(@babel/core@7.29.7)(@playwright/test@1.58.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(prisma@6.7.0(typescript@5.9.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(better-call@1.3.5(zod@4.3.6))
'@google-cloud/storage':
specifier: ^7.19.0
version: 7.21.0
'@hookform/resolvers':
specifier: ^5.4.0
version: 5.4.0(react-hook-form@7.77.0(react@19.2.7))
@@ -1508,6 +1511,22 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@google-cloud/paginator@5.0.2':
resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==}
engines: {node: '>=14.0.0'}
'@google-cloud/projectify@4.0.0':
resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==}
engines: {node: '>=14.0.0'}
'@google-cloud/promisify@4.0.0':
resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==}
engines: {node: '>=14'}
'@google-cloud/storage@7.21.0':
resolution: {integrity: sha512-l+IFTkd+6Y5LoAuXyYCKNAKtw/Ci+rAMqgdTB1jv4iZiLhw0rtq+0qjIRbBizXkNzEFmXiXUW0H7sZQQvk1ffA==}
engines: {node: '>=14'}
'@grpc/grpc-js@1.14.4':
resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==}
engines: {node: '>=12.10.0'}
@@ -3856,6 +3875,10 @@ packages:
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
engines: {node: '>=12'}
'@tootallnate/once@2.0.1':
resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==}
engines: {node: '>= 10'}
'@tootallnate/quickjs-emscripten@0.23.0':
resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
@@ -3871,6 +3894,9 @@ packages:
'@types/canvas-confetti@1.9.0':
resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
'@types/caseless@0.12.5':
resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==}
'@types/cors@2.8.19':
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
@@ -3963,6 +3989,12 @@ packages:
'@types/react@19.2.17':
resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==}
'@types/request@2.48.13':
resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
'@types/webpack@5.28.5':
resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==}
@@ -4254,6 +4286,10 @@ packages:
'@zenstackhq/tanstack-query@2.22.2':
resolution: {integrity: sha512-VXYc9MyJRuEbwvkcIKwsgKjKSW44Gh19YPNGarR5//IfjZ2+uPf916vrOiOXb6Qzyc9aU3PriDYFrkZY5enK1g==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -4393,6 +4429,10 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
arrify@2.0.1:
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
engines: {node: '>=8'}
asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
@@ -4421,6 +4461,9 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
@@ -4806,6 +4849,10 @@ packages:
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
engines: {node: '>=0.1.90'}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@13.1.0:
resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==}
engines: {node: '>=18'}
@@ -5094,6 +5141,10 @@ packages:
resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
engines: {node: '>= 14'}
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -5267,6 +5318,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexify@4.1.3:
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -5579,6 +5633,10 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
@@ -5722,6 +5780,10 @@ packages:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@2.5.6:
resolution: {integrity: sha512-Ogz/E85h9tlfJzpI6TuFpGcHZFhLrb9Gw8wq9v40CxSCPnv7ahKr6Xgtkn0KYCDQJ8DNn5VoMO8EXr9V5PadyA==}
engines: {node: '>= 0.12'}
format@0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
@@ -5796,6 +5858,10 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
gaxios@6.7.1:
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
engines: {node: '>=14'}
gaxios@7.1.3:
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
engines: {node: '>=18'}
@@ -5804,6 +5870,10 @@ packages:
resolution: {integrity: sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==}
engines: {node: '>=18'}
gcp-metadata@6.1.1:
resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
engines: {node: '>=14'}
gcp-metadata@8.1.2:
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
engines: {node: '>=18'}
@@ -5915,6 +5985,14 @@ packages:
resolution: {integrity: sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==}
engines: {node: '>=18'}
google-auth-library@9.15.1:
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
engines: {node: '>=14'}
google-logging-utils@0.0.2:
resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
engines: {node: '>=14'}
google-logging-utils@1.1.3:
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
engines: {node: '>=14'}
@@ -5934,6 +6012,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
gtoken@7.1.0:
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
engines: {node: '>=14.0.0'}
gtoken@8.0.0:
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
engines: {node: '>=18'}
@@ -5997,6 +6079,9 @@ packages:
resolution: {integrity: sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==}
engines: {node: ^18.17.0 || >=20.5.0}
html-entities@2.6.0:
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
@@ -6011,6 +6096,10 @@ packages:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
@@ -6215,6 +6304,10 @@ packages:
is-ssh@1.4.1:
resolution: {integrity: sha512-JNeu1wQsHjyHgn9NcWTaXq6zWSR6hqE0++zhfZlkFBbScNkyvxCdeV8sRkSBaeLKxmbpR21brail63ACNxJ0Tg==}
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
is-stream@3.0.0:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -6628,6 +6721,11 @@ packages:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
mime@3.0.0:
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
engines: {node: '>=10.0.0'}
hasBin: true
mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@@ -7600,6 +7698,10 @@ packages:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
retry-request@7.0.2:
resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==}
engines: {node: '>=14'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
@@ -7881,9 +7983,15 @@ packages:
stream-chain@2.2.5:
resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
stream-events@1.0.5:
resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==}
stream-json@1.9.1:
resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
stream-shift@1.0.3:
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
strict-uri-encode@2.0.0:
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
engines: {node: '>=4'}
@@ -7961,6 +8069,9 @@ packages:
strnum@2.3.0:
resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==}
stubs@3.0.0:
resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -8037,6 +8148,10 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'}
teeny-request@9.0.0:
resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==}
engines: {node: '>=14'}
terminal-link@2.1.1:
resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}
engines: {node: '>=8'}
@@ -9513,6 +9628,35 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
'@google-cloud/paginator@5.0.2':
dependencies:
arrify: 2.0.1
extend: 3.0.2
'@google-cloud/projectify@4.0.0': {}
'@google-cloud/promisify@4.0.0': {}
'@google-cloud/storage@7.21.0':
dependencies:
'@google-cloud/paginator': 5.0.2
'@google-cloud/projectify': 4.0.0
'@google-cloud/promisify': 4.0.0
abort-controller: 3.0.0
async-retry: 1.3.3
duplexify: 4.1.3
fast-xml-parser: 5.8.0
gaxios: 6.7.1
google-auth-library: 9.15.1
html-entities: 2.6.0
mime: 3.0.0
p-limit: 3.1.0
retry-request: 7.0.2
teeny-request: 9.0.0
transitivePeerDependencies:
- encoding
- supports-color
'@grpc/grpc-js@1.14.4':
dependencies:
'@grpc/proto-loader': 0.8.1
@@ -11860,6 +12004,8 @@ snapshots:
'@tanstack/table-core@8.21.3': {}
'@tootallnate/once@2.0.1': {}
'@tootallnate/quickjs-emscripten@0.23.0': {}
'@ts-morph/common@0.17.0':
@@ -11882,6 +12028,8 @@ snapshots:
'@types/canvas-confetti@1.9.0': {}
'@types/caseless@0.12.5': {}
'@types/cors@2.8.19':
dependencies:
'@types/node': 22.19.20
@@ -11977,11 +12125,20 @@ snapshots:
dependencies:
csstype: 3.2.3
'@types/request@2.48.13':
dependencies:
'@types/caseless': 0.12.5
'@types/node': 22.19.20
'@types/tough-cookie': 4.0.5
form-data: 2.5.6
'@types/tough-cookie@4.0.5': {}
'@types/webpack@5.28.5(esbuild@0.25.10)(postcss@8.5.10)':
dependencies:
'@types/node': 22.19.20
tapable: 2.3.3
webpack: 5.107.2(esbuild@0.25.10)(postcss@8.5.10)
webpack: 5.107.2(esbuild@0.27.7)(postcss@8.5.10)
transitivePeerDependencies:
- '@minify-html/node'
- '@swc/core'
@@ -12406,6 +12563,10 @@ snapshots:
- typescript
- zod
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -12567,6 +12728,8 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
arrify@2.0.1: {}
asn1@0.2.6:
dependencies:
safer-buffer: 2.1.2
@@ -12593,6 +12756,8 @@ snapshots:
async@3.2.6: {}
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
attr-accept@2.2.5: {}
@@ -12995,6 +13160,10 @@ snapshots:
colors@1.4.0: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
commander@13.1.0: {}
commander@2.20.3: {}
@@ -13256,6 +13425,8 @@ snapshots:
escodegen: 2.1.0
esprima: 4.0.1
delayed-stream@1.0.0: {}
depd@2.0.0: {}
destr@2.0.5: {}
@@ -13357,6 +13528,13 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexify@4.1.3:
dependencies:
end-of-stream: 1.4.5
inherits: 2.0.4
readable-stream: 3.6.2
stream-shift: 1.0.3
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
@@ -13726,7 +13904,7 @@ snapshots:
'@next/eslint-plugin-next': 16.2.7
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
@@ -13749,7 +13927,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@@ -13764,14 +13942,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
eslint-module-utils@2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies:
- supports-color
@@ -13786,7 +13964,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-module-utils: 2.13.0(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
hasown: 2.0.4
is-core-module: 2.16.2
is-glob: 4.0.3
@@ -13947,6 +14125,8 @@ snapshots:
etag@1.8.1: {}
event-target-shim@5.0.1: {}
eventemitter3@4.0.7: {}
eventemitter3@5.0.4: {}
@@ -14129,6 +14309,15 @@ snapshots:
cross-spawn: 7.0.6
signal-exit: 4.1.0
form-data@2.5.6:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
hasown: 2.0.4
mime-types: 2.1.35
safe-buffer: 5.2.1
format@0.2.2: {}
formdata-polyfill@4.0.10:
@@ -14186,6 +14375,17 @@ snapshots:
functions-have-names@1.2.3: {}
gaxios@6.7.1:
dependencies:
extend: 3.0.2
https-proxy-agent: 7.0.6
is-stream: 2.0.1
node-fetch: 2.7.0
uuid: 9.0.1
transitivePeerDependencies:
- encoding
- supports-color
gaxios@7.1.3:
dependencies:
extend: 3.0.2
@@ -14203,6 +14403,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
gcp-metadata@6.1.1:
dependencies:
gaxios: 6.7.1
google-logging-utils: 0.0.2
json-bigint: 1.0.0
transitivePeerDependencies:
- encoding
- supports-color
gcp-metadata@8.1.2:
dependencies:
gaxios: 7.1.5
@@ -14330,7 +14539,7 @@ snapshots:
dependencies:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
gaxios: 7.1.3
gaxios: 7.1.5
gcp-metadata: 8.1.3
google-logging-utils: 1.1.3
gtoken: 8.0.0
@@ -14349,6 +14558,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
google-auth-library@9.15.1:
dependencies:
base64-js: 1.5.1
ecdsa-sig-formatter: 1.0.11
gaxios: 6.7.1
gcp-metadata: 6.1.1
gtoken: 7.1.0
jws: 4.0.1
transitivePeerDependencies:
- encoding
- supports-color
google-logging-utils@0.0.2: {}
google-logging-utils@1.1.3: {}
googleapis-common@8.0.2:
@@ -14373,9 +14596,17 @@ snapshots:
graceful-fs@4.2.11: {}
gtoken@7.1.0:
dependencies:
gaxios: 6.7.1
jws: 4.0.1
transitivePeerDependencies:
- encoding
- supports-color
gtoken@8.0.0:
dependencies:
gaxios: 7.1.3
gaxios: 7.1.5
jws: 4.0.1
transitivePeerDependencies:
- supports-color
@@ -14434,6 +14665,8 @@ snapshots:
dependencies:
lru-cache: 10.4.3
html-entities@2.6.0: {}
html-to-text@9.0.5:
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
@@ -14464,6 +14697,14 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
http-proxy-agent@5.0.0:
dependencies:
'@tootallnate/once': 2.0.1
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
@@ -14655,6 +14896,8 @@ snapshots:
dependencies:
protocols: 2.0.2
is-stream@2.0.1: {}
is-stream@3.0.0: {}
is-string@1.1.1:
@@ -15006,6 +15249,8 @@ snapshots:
dependencies:
mime-db: 1.54.0
mime@3.0.0: {}
mimic-fn@2.1.0: {}
mimic-fn@4.0.0: {}
@@ -16097,6 +16342,15 @@ snapshots:
onetime: 7.0.0
signal-exit: 4.1.0
retry-request@7.0.2:
dependencies:
'@types/request': 2.48.13
extend: 3.0.2
teeny-request: 9.0.0
transitivePeerDependencies:
- encoding
- supports-color
retry@0.13.1: {}
reusify@1.1.0: {}
@@ -16496,10 +16750,16 @@ snapshots:
stream-chain@2.2.5: {}
stream-events@1.0.5:
dependencies:
stubs: 3.0.0
stream-json@1.9.1:
dependencies:
stream-chain: 2.2.5
stream-shift@1.0.3: {}
strict-uri-encode@2.0.0: {}
string-width@4.2.3:
@@ -16600,6 +16860,8 @@ snapshots:
strnum@2.3.0: {}
stubs@3.0.0: {}
styled-jsx@5.1.6(@babel/core@7.26.10)(react@19.0.0):
dependencies:
client-only: 0.0.1
@@ -16706,18 +16968,29 @@ snapshots:
inherits: 2.0.4
readable-stream: 3.6.2
teeny-request@9.0.0:
dependencies:
http-proxy-agent: 5.0.0
https-proxy-agent: 5.0.0
node-fetch: 2.7.0
stream-events: 1.0.5
uuid: 9.0.1
transitivePeerDependencies:
- encoding
- supports-color
terminal-link@2.1.1:
dependencies:
ansi-escapes: 4.3.2
supports-hyperlinks: 2.3.0
terser-webpack-plugin@5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10)):
terser-webpack-plugin@5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10)):
dependencies:
'@jridgewell/trace-mapping': 0.3.31
jest-worker: 27.5.1
schema-utils: 4.3.3
terser: 5.48.0
webpack: 5.107.2(esbuild@0.25.10)(postcss@8.5.10)
webpack: 5.107.2(esbuild@0.27.7)(postcss@8.5.10)
optionalDependencies:
esbuild: 0.25.10
postcss: 8.5.10
@@ -17114,7 +17387,7 @@ snapshots:
webpack-sources@3.5.0: {}
webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10):
webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10):
dependencies:
'@types/estree': 1.0.9
'@types/json-schema': 7.0.15
@@ -17136,7 +17409,7 @@ snapshots:
neo-async: 2.6.2
schema-utils: 4.3.3
tapable: 2.3.3
terser-webpack-plugin: 5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.25.10)(postcss@8.5.10))
terser-webpack-plugin: 5.6.1(esbuild@0.25.10)(postcss@8.5.10)(webpack@5.107.2(esbuild@0.27.7)(postcss@8.5.10))
watchpack: 2.5.1
webpack-sources: 3.5.0
transitivePeerDependencies:
+1
View File
@@ -0,0 +1 @@
ALTER TYPE "public"."provider_storage_kind" ADD VALUE 'google-cloud-storage' BEFORE 'blob';
File diff suppressed because it is too large Load Diff
+7
View File
@@ -477,6 +477,13 @@
"when": 1782573099089,
"tag": "0067_clammy_stephen_strange",
"breakpoints": true
},
{
"idx": 68,
"version": "7",
"when": 1782575192480,
"tag": "0068_busy_zeigeist",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -5,7 +5,7 @@ import {relations} from "drizzle-orm";
import {createSelectSchema} from "drizzle-zod";
import {z} from "zod";
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive', 'blob']);
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive', 'google-cloud-storage', 'blob']);
export const storageChannel = pgTable('storage_channel', {
id: uuid("id").defaultRandom().primaryKey(),
@@ -44,6 +44,10 @@ import {
import {
StorageBlobForm
} from "@/features/channel/components/storages/az-blob.form";
import {
StorageGoogleCloudStorageForm
} from "@/features/channel/components/storages/google-cloud-storage/google-cloud-storage.form";
export type ChannelKind = "notification" | "storage";
@@ -109,6 +113,8 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet
return <StorageS3Form form={form}/>
case "google-drive":
return <StorageGoogleDriveForm form={form}/>
case "google-cloud-storage":
return <StorageGoogleCloudStorageForm form={form}/>
case "blob":
return <StorageBlobForm form={form}/>
case "local":
@@ -7,7 +7,7 @@ export const storageProviders: ProviderIconTypes[] = [
{value: "s3", label: "S3", icon: S3Icon},
{value: "google-drive", label: "Google Drive", icon: GoogleDriveIcon},
{value: "blob", label: "Azure Blob Storage", icon: BlobIcon},
{value: "gcs", label: "Google Cloud Storage", icon: GCSIcon, preview: true},
{value: "google-cloud-storage", label: "Google Cloud Storage", icon: GCSIcon},
]
export function S3Icon(props: SVGProps<SVGSVGElement>) {
@@ -0,0 +1,103 @@
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
type StorageGoogleCloudStorageFormProps = {
form: UseFormReturn<any, any, any>;
};
export const StorageGoogleCloudStorageForm = ({
form,
}: StorageGoogleCloudStorageFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.projectId"
render={({ field }) => (
<FormItem>
<FormLabel>Project ID *</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ""} placeholder="e.g. my-gcp-project" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.bucketName"
render={({ field }) => (
<FormItem>
<FormLabel>Bucket name *</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ""} placeholder="e.g. backups-prod" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.clientEmail"
render={({ field }) => (
<FormItem>
<FormLabel>Client email *</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
placeholder="e.g. service-account@my-gcp-project.iam.gserviceaccount.com"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.apiEndpoint"
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint URL</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
placeholder="e.g. http://localhost:4443"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.privateKey"
render={({ field }) => (
<FormItem>
<FormLabel>Private key *</FormLabel>
<FormControl>
<Textarea
{...field}
value={field.value ?? ""}
rows={5}
placeholder="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
};
@@ -0,0 +1,12 @@
import {z} from "zod";
export const GoogleCloudStorageChannelConfigSchema = z.object({
projectId: z.string().trim().min(1, "Project ID is required"),
bucketName: z.string().trim().min(1, "Bucket name is required"),
clientEmail: z.email("Client email must be a valid email").trim(),
privateKey: z.string().trim().min(1, "Private key is required"),
apiEndpoint: z.preprocess(
(v) => (v === "" ? undefined : v),
z.string().url("Endpoint URL must be a valid URL").optional(),
),
});
@@ -0,0 +1,179 @@
import {Storage} from "@google-cloud/storage";
import {Readable} from "node:stream";
import {GoogleCloudStorageConfig} from "@/features/channel/components/storages/google-cloud-storage/types";
import {
StorageCopyInput,
StorageDeleteInput,
StorageGetInput,
StorageMetaData,
StorageResult,
StorageUploadInput
} from "@/features/storages/types";
async function getGoogleCloudStorageClient(config: GoogleCloudStorageConfig) {
return new Storage({
projectId: config.projectId,
...(config.apiEndpoint ? {apiEndpoint: config.apiEndpoint} : {}),
credentials: {
client_email: config.clientEmail,
private_key: config.privateKey.replace(/\\n/g, "\n"),
},
});
}
const BASE_DIR = "";
async function ensureBucket(config: GoogleCloudStorageConfig) {
const client = await getGoogleCloudStorageClient(config);
const [exists] = await client.bucket(config.bucketName).exists();
if (!exists) throw new Error(`Bucket "${config.bucketName}" does not exist`);
}
export async function uploadGoogleCloudStorage(
config: GoogleCloudStorageConfig,
input: { data: StorageUploadInput, metadata?: StorageMetaData }
): Promise<StorageResult> {
const client = await getGoogleCloudStorageClient(config);
await ensureBucket(config);
const key = `${BASE_DIR}${input.data.path}`;
const file = input.data.file;
let uploadStream: Readable;
if (Buffer.isBuffer(file) || file instanceof Uint8Array) {
uploadStream = Readable.from(file);
} else if ((file as any).pipe) {
uploadStream = file as Readable;
} else {
return {success: false, provider: "google-cloud-storage", error: "Unsupported file type for streaming upload"};
}
try {
const writeStream = client
.bucket(config.bucketName)
.file(key)
.createWriteStream({contentType: input.data.contentType});
await new Promise<void>((resolve, reject) => {
uploadStream
.pipe(writeStream)
.on("finish", resolve)
.on("error", reject);
});
} catch (err: any) {
return {success: false, provider: "google-cloud-storage", error: err.message};
}
return {success: true, provider: "google-cloud-storage"};
}
export async function getGoogleCloudStorage(
config: GoogleCloudStorageConfig,
input: { data: StorageGetInput, metadata: StorageMetaData }
): Promise<StorageResult> {
const client = await getGoogleCloudStorageClient(config);
const key = `${BASE_DIR}${input.data.path}`;
const file = client.bucket(config.bucketName).file(key);
const [exists] = await file.exists();
if (!exists) return {success: false, provider: "google-cloud-storage", error: "File not found"};
const fileStream = file.createReadStream();
let signedUrl: string | undefined;
if (input.data.signedUrl) {
if (config.apiEndpoint) {
signedUrl = `${config.apiEndpoint.replace(/\/$/, "")}/${config.bucketName}/${encodeURI(key)}`;
} else {
const [url] = await file.getSignedUrl({
action: "read",
expires: Date.now() + (input.data.expiresInSeconds ?? 60) * 1000,
});
signedUrl = url;
}
}
return {
success: true,
provider: "google-cloud-storage",
file: fileStream as unknown as Buffer | Readable,
url: signedUrl,
};
}
export async function deleteGoogleCloudStorage(config: GoogleCloudStorageConfig, input: {
data: StorageDeleteInput,
metadata?: StorageMetaData
}): Promise<StorageResult> {
const client = await getGoogleCloudStorageClient(config);
const key = `${BASE_DIR}${input.data.path}`;
try {
await client.bucket(config.bucketName).file(key).delete();
return {success: true, provider: "google-cloud-storage"};
} catch (err: any) {
return {success: false, provider: "google-cloud-storage", error: err.message};
}
}
export async function pingGoogleCloudStorage(config: GoogleCloudStorageConfig): Promise<StorageResult> {
try {
const client = await getGoogleCloudStorageClient(config);
const bucket = client.bucket(config.bucketName);
const [exists] = await bucket.exists();
if (!exists) return {
success: false,
provider: "google-cloud-storage",
response: "Bucket does not exist"
};
const key = `${BASE_DIR}ping.txt`;
const file = bucket.file(key);
await file.save(Buffer.from("ping"));
await file.download();
await file.delete();
return {
success: true,
provider: "google-cloud-storage",
response: "Google Cloud Storage OK"
};
} catch (err: any) {
return {
success: false,
provider: "google-cloud-storage",
response: err.message
};
}
}
export async function copyGoogleCloudStorage(
config: GoogleCloudStorageConfig,
input: {
data: StorageCopyInput,
},
): Promise<StorageResult> {
const client = await getGoogleCloudStorageClient(config);
await ensureBucket(config);
const sourceKey = `${BASE_DIR}${input.data.from}`;
const destinationKey = `${BASE_DIR}${input.data.to}`;
try {
const bucket = client.bucket(config.bucketName);
await bucket.file(sourceKey).copy(bucket.file(destinationKey));
return {
success: true,
provider: "google-cloud-storage",
};
} catch (err: any) {
return {
success: false,
provider: "google-cloud-storage",
error: err.message,
};
}
}
@@ -0,0 +1,7 @@
export type GoogleCloudStorageConfig = {
projectId: string;
bucketName: string;
clientEmail: string;
privateKey: string;
apiEndpoint: string;
};
@@ -14,6 +14,13 @@ import {
pingGoogleDrive,
uploadGoogleDrive
} from "@/features/channel/components/storages/google-drive";
import {
copyGoogleCloudStorage,
deleteGoogleCloudStorage,
getGoogleCloudStorage,
pingGoogleCloudStorage,
uploadGoogleCloudStorage
} from "@/features/channel/components/storages/google-cloud-storage";
type ProviderHandler = {
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
@@ -51,6 +58,13 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
delete: deleteBlob,
ping: pingBlob,
copy: copyBlob,
},
"google-cloud-storage": {
upload: uploadGoogleCloudStorage,
get: getGoogleCloudStorage,
delete: deleteGoogleCloudStorage,
ping: pingGoogleCloudStorage,
copy: copyGoogleCloudStorage,
}
};
@@ -13,6 +13,10 @@ import {GoogleDriveChannelConfigSchema} from "@/features/channel/components/stor
import {LocalChannelConfigSchema} from "@/features/channel/components/storages/local.schema";
import {TeamsChannelConfigSchema} from "@/features/channel/components/notifications/teams.schema";
import {BlobChannelConfigSchema} from "@/features/channel/components/storages/az-blob.schema";
import {
GoogleCloudStorageChannelConfigSchema
} from "@/features/channel/components/storages/google-cloud-storage/google-cloud-storage.schema";
const BaseChannelFormSchema = z.object({
@@ -79,6 +83,10 @@ export const StorageChannelFormSchema = z.discriminatedUnion("provider", [
provider: z.literal("blob"),
config: BlobChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("google-cloud-storage"),
config: GoogleCloudStorageChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("local"),
config: LocalChannelConfigSchema
+1
View File
@@ -4,6 +4,7 @@ export type StorageProviderKind =
| 'local'
| 's3'
| 'google-drive'
| 'google-cloud-storage'
| 'blob'
;