Commit Diff


commit - 4786b57869b935b953a4a2f4718b9e26d95341e4
commit + b7a2afe9432db697e6ef4b7838e83490a335583e
blob - 66b165f768c52a86dd4b0121a2503095c024f02e
blob + 4075874c196d5d7ee0ba1e4a035f1889e9ebb2ac
--- flake.nix
+++ flake.nix
@@ -58,7 +58,7 @@
         name = "corpus-pnpm-source";
       };
       pname = "corpus";
-      hash = "sha256-7xgw8iQ1d69rQCiiyuRXMk5phMg2df5NHfzKGDboxak=";
+      hash = "sha256-H8XTwSL9ura4VYTrBQwqtxt881TDBXOZyoUzAHDvep4=";
       fetcherVersion = 3;
     };
 
blob - 18507367c4224ea0c1abfe914d1636253a5be6f6
blob + 7737fe8863d18162d43437c95254ec7a37d30f48
--- package.json
+++ package.json
@@ -1,31 +1,32 @@
 {
-  "name": "corpus",
-  "version": "2.18.1",
-  "description": "ListenBrainz and Last.fm frontend",
-  "main": "index.js",
-  "type": "module",
-  "scripts": {
-    "build": "spago build && esbuild output/Main/index.js --bundle --platform=node --format=esm --footer:js='main();' --outfile=server.js --external:http --external:https --external:dotenv --external:url --external:@duckdb/node-api --external:@aws-sdk/client-s3 --external:prom-client --external:sharp --external:uuid && elm make src/Client.elm --output=client.js",
-    "release": "spago build && purs-backend-es build && esbuild output-es/Main/index.js --bundle --platform=node --format=esm --footer:js='main();' --outfile=server.js --external:http --external:https --external:dotenv --external:url --external:@duckdb/node-api --external:@aws-sdk/client-s3 --external:prom-client --external:sharp --external:uuid && elm make src/Client.elm --optimize --output=client.js && uglifyjs client.js --compress \"pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe\" | uglifyjs --mangle --output client.js",
-    "test": "spago test",
-    "tidy": "purs-tidy format-in-place src/**/*.purs"
-  },
-  "devDependencies": {
-    "esbuild": "^0.28.0",
-    "purescript-language-server": "^0.18.5",
-    "purescript-psa": "^0.9.0",
-    "purs-backend-es": "^1.4.3",
-    "purs-tidy": "^0.11.1",
-    "spago": "^1.0.4",
-    "uglify-js": "^3.19.3",
-    "whine": "^0.0.33"
-  },
-  "dependencies": {
-    "@aws-sdk/client-s3": "^3.1061.0",
-    "@duckdb/node-api": "1.5.2-r.2",
-    "dotenv": "^17.4.2",
-    "prom-client": "^15.1.3",
-    "sharp": "^0.34.5",
-    "uuid": "^14.0.0"
-  }
+	"name": "corpus",
+	"version": "2.18.1",
+	"description": "ListenBrainz and Last.fm frontend",
+	"main": "index.js",
+	"type": "module",
+	"scripts": {
+		"build": "spago build && esbuild output/Main/index.js --bundle --platform=node --format=esm --footer:js='main();' --outfile=server.js --external:http --external:https --external:dotenv --external:url --external:@duckdb/node-api --external:@aws-sdk/client-s3 --external:@aws-sdk/s3-request-presigner --external:prom-client --external:sharp --external:uuid && elm make src/Client.elm --output=client.js",
+		"release": "spago build && purs-backend-es build && esbuild output-es/Main/index.js --bundle --platform=node --format=esm --footer:js='main();' --outfile=server.js --external:http --external:https --external:dotenv --external:url --external:@duckdb/node-api --external:@aws-sdk/client-s3 --external:@aws-sdk/s3-request-presigner --external:prom-client --external:sharp --external:uuid && elm make src/Client.elm --optimize --output=client.js && uglifyjs client.js --compress \"pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe\" | uglifyjs --mangle --output client.js",
+		"test": "spago test",
+		"tidy": "purs-tidy format-in-place src/**/*.purs"
+	},
+	"devDependencies": {
+		"esbuild": "^0.28.0",
+		"purescript-language-server": "^0.18.5",
+		"purescript-psa": "^0.9.0",
+		"purs-backend-es": "^1.4.3",
+		"purs-tidy": "^0.11.1",
+		"spago": "^1.0.4",
+		"uglify-js": "^3.19.3",
+		"whine": "^0.0.33"
+	},
+	"dependencies": {
+		"@aws-sdk/client-s3": "^3.1061.0",
+		"@aws-sdk/s3-request-presigner": "^3.1063.0",
+		"@duckdb/node-api": "1.5.2-r.2",
+		"dotenv": "^17.4.2",
+		"prom-client": "^15.1.3",
+		"sharp": "^0.34.5",
+		"uuid": "^14.0.0"
+	}
 }
blob - 1f2ea5c1c016d124efa4f1dc2df517fbd98d3151
blob + a31723311a32fd614b0bda3d90d2b09a871c777d
--- pnpm-lock.yaml
+++ pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
       '@aws-sdk/client-s3':
         specifier: ^3.1061.0
         version: 3.1061.0
+      '@aws-sdk/s3-request-presigner':
+        specifier: ^3.1063.0
+        version: 3.1063.0
       '@duckdb/node-api':
         specifier: 1.5.2-r.2
         version: 1.5.2-r.2
@@ -89,6 +92,10 @@ packages:
     resolution: {integrity: sha512-r8o4h2K7j6P9ngno+8ei0aK0U/4JwDb7A2fMMxGVoSqDN8AFlIzSDeZHME9LcVLR2codyhtr1WAAg+/nmkeeMA==}
     engines: {node: '>=20.0.0'}
 
+  '@aws-sdk/core@3.974.18':
+    resolution: {integrity: sha512-JDYCPI0j7zGrzXTDFsLB346cxss7J/AxH7+O0MzWlqppJBEyB9Qe6TQXRL6iwLUo/xZkNv9KFmBL2hqElmwW0g==}
+    engines: {node: '>=20.0.0'}
+
   '@aws-sdk/credential-provider-env@3.972.43':
     resolution: {integrity: sha512-g0XVQKzaA/4cq1vz1IvCQwYM+1Pkv01J9yHDpCTXekVuGZRDEz0wqBQ1AuYTq7FM6uik4uBGH8Tb5d9YvgeA7g==}
     engines: {node: '>=20.0.0'}
@@ -149,10 +156,18 @@ packages:
     resolution: {integrity: sha512-Fpri1/PXKMKveORZ7E00VLTlWS5DkfZkW70PUE+bOnpWpAeHAQLoiDHhkzN3kNWbbSsGg64+IZYiq/EZgME3Mg==}
     engines: {node: '>=20.0.0'}
 
+  '@aws-sdk/s3-request-presigner@3.1063.0':
+    resolution: {integrity: sha512-uYDuWEbBYz/DfS0sEbDA3BeBCNNaLiE+dmblIRrmU02bhoqsotJb3qWfx0+I3JlukC7Vk8wRuyRgjZzq1FyXGA==}
+    engines: {node: '>=20.0.0'}
+
   '@aws-sdk/signature-v4-multi-region@3.996.31':
     resolution: {integrity: sha512-Kn2up9SlG1KC6wRtwf0d7waTGF6rvp9DxYqB54x6UCKdQ6kyaXCqHL4WGb5vUJga5kS8FxnjhY0LqM28aMvnNQ==}
     engines: {node: '>=20.0.0'}
 
+  '@aws-sdk/signature-v4-multi-region@3.996.32':
+    resolution: {integrity: sha512-llvApLcsWtmRFhG2wT3WIp1CmDeRaIYutqty1ZZXoMzK7TiJ6MOLOimk9eXUS8PwgG4ew4pa4QAbt0lfhn++1w==}
+    engines: {node: '>=20.0.0'}
+
   '@aws-sdk/token-providers@3.1060.0':
     resolution: {integrity: sha512-6NZaMKkFhpaNiwLpHi1sZaYjidL/lCJE6ME6NxwA8gv9vQna+Kr0j4OFwVoz6tANRWM3WbGz6jiPsGX/Vkjwow==}
     engines: {node: '>=20.0.0'}
@@ -161,6 +176,10 @@ packages:
     resolution: {integrity: sha512-992QrTO7G9qCvKD0fx1rMlqcL14plUcRAbwmqqYVsuF3GrqcvlAL9qxR+baMafarEZ+l7DUQ5lCMmt5mbMhF7g==}
     engines: {node: '>=20.0.0'}
 
+  '@aws-sdk/types@3.973.11':
+    resolution: {integrity: sha512-YjS0qFuECClRh4qhEyW8XagW0fwEPBeZ1cfsW/gU73Kh/ExFILxbzxOfPCmzF/2DwEvhvsHYt0b0qnvStwKYrg==}
+    engines: {node: '>=20.0.0'}
+
   '@aws-sdk/util-locate-window@3.965.5':
     resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==}
     engines: {node: '>=20.0.0'}
@@ -169,6 +188,10 @@ packages:
     resolution: {integrity: sha512-hpsCXCOI436kxWpjtRuIHVvuPP81MOw8f18jzfZeg+UOiiOvlqWcmWChzEhJEu16cOC6+ku4ncBN+7rdt+DZ9g==}
     engines: {node: '>=20.0.0'}
 
+  '@aws-sdk/xml-builder@3.972.28':
+    resolution: {integrity: sha512-lI/l3c/vPvsxmspzV63NfS3x9q4CkMmdhJy4QiM+NThAufVkDvi/PZZQ6xETnICL0UD7jI808pY83gllf86RFg==}
+    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'}
@@ -1089,6 +1112,17 @@ snapshots:
       bowser: 2.14.1
       tslib: 2.8.1
 
+  '@aws-sdk/core@3.974.18':
+    dependencies:
+      '@aws-sdk/types': 3.973.11
+      '@aws-sdk/xml-builder': 3.972.28
+      '@aws/lambda-invoke-store': 0.2.4
+      '@smithy/core': 3.24.6
+      '@smithy/signature-v4': 5.4.6
+      '@smithy/types': 4.14.3
+      bowser: 2.14.1
+      tslib: 2.8.1
+
   '@aws-sdk/credential-provider-env@3.972.43':
     dependencies:
       '@aws-sdk/core': 3.974.17
@@ -1220,6 +1254,15 @@ snapshots:
       '@smithy/types': 4.14.3
       tslib: 2.8.1
 
+  '@aws-sdk/s3-request-presigner@3.1063.0':
+    dependencies:
+      '@aws-sdk/core': 3.974.18
+      '@aws-sdk/signature-v4-multi-region': 3.996.32
+      '@aws-sdk/types': 3.973.11
+      '@smithy/core': 3.24.6
+      '@smithy/types': 4.14.3
+      tslib: 2.8.1
+
   '@aws-sdk/signature-v4-multi-region@3.996.31':
     dependencies:
       '@aws-sdk/types': 3.973.10
@@ -1227,6 +1270,13 @@ snapshots:
       '@smithy/types': 4.14.3
       tslib: 2.8.1
 
+  '@aws-sdk/signature-v4-multi-region@3.996.32':
+    dependencies:
+      '@aws-sdk/types': 3.973.11
+      '@smithy/signature-v4': 5.4.6
+      '@smithy/types': 4.14.3
+      tslib: 2.8.1
+
   '@aws-sdk/token-providers@3.1060.0':
     dependencies:
       '@aws-sdk/core': 3.974.17
@@ -1241,6 +1291,11 @@ snapshots:
       '@smithy/types': 4.14.3
       tslib: 2.8.1
 
+  '@aws-sdk/types@3.973.11':
+    dependencies:
+      '@smithy/types': 4.14.3
+      tslib: 2.8.1
+
   '@aws-sdk/util-locate-window@3.965.5':
     dependencies:
       tslib: 2.8.1
@@ -1251,6 +1306,12 @@ snapshots:
       fast-xml-parser: 5.7.3
       tslib: 2.8.1
 
+  '@aws-sdk/xml-builder@3.972.28':
+    dependencies:
+      '@smithy/types': 4.14.3
+      fast-xml-parser: 5.7.3
+      tslib: 2.8.1
+
   '@aws/lambda-invoke-store@0.2.4': {}
 
   '@duckdb/node-api@1.5.2-r.2':
blob - a2a63913946430b56fa9bf5c1c79381e49816e7c
blob + ebdcdda05656016d927a3cec8cbae96fda53c7f4
--- src/Cover.purs
+++ src/Cover.purs
@@ -11,7 +11,7 @@ import Prelude
 
 import Control.Alt ((<|>))
 import Config (UserConfig, s3ConfigFromUser)
-import S3 (existsInS3, getS3Url, uploadToS3)
+import S3 (existsInS3, getPresignedUrl, uploadToS3)
 import Data.Array ((!!), find)
 import Data.Either (Either(..), fromRight')
 import Data.Foldable (foldM)
@@ -181,7 +181,7 @@ serveCover serveNotFound cfg slug url res = do
     if cached then do
       Log.info $ "Serving " <> name <> " cover from cache: " <> s3Key
       liftEffect $ Metrics.incCoverRequest slug name "s3_hit"
-      liftEffect $ serveS3Redirect s3cfg s3Key res
+      serveS3Redirect s3cfg s3Key res
       pure true
     else do
       mUrl <- findUrl
@@ -202,9 +202,11 @@ serveCover serveNotFound cfg slug url res = do
           Left _ -> false
 
   serveS3Redirect s3cfg s3Key response = do
-    setStatusCode 302 response
-    setHeader "Location" (getS3Url s3cfg s3Key) (toOutgoingMessage response)
-    end (toWriteable (toOutgoingMessage response))
+    presignedUrl <- getPresignedUrl s3cfg s3Key
+    liftEffect $ do
+      setStatusCode 302 response
+      setHeader "Location" presignedUrl (toOutgoingMessage response)
+      end (toWriteable (toOutgoingMessage response))
 
   -- Redirect the client immediately to the upstream URL, then fetch+convert+cache in background.
   -- This avoids blocking the response on AVIF conversion (which can take hundreds of ms).
blob - 428239c117240fe1d876654f8088ede59546c979
blob + d6243865ab95c8afde0b0abd5afe6420cfa7ece5
--- src/S3.js
+++ src/S3.js
@@ -2,7 +2,9 @@ import {
 	S3Client,
 	PutObjectCommand,
 	HeadObjectCommand,
+	GetObjectCommand,
 } from "@aws-sdk/client-s3";
+import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
 
 const makeClient = (cfg) =>
 	new S3Client({
@@ -52,6 +54,18 @@ export const existsInS3Impl = (cfg, key, cb) => () => 
 		});
 };
 
+export const getPresignedUrlImpl = (cfg, key, cb) => () => {
+	const client = makeClient(cfg);
+	const command = new GetObjectCommand({
+		Bucket: cfg.bucket,
+		Key: key,
+	});
+
+	getSignedUrl(client, command, { expiresIn: 86400 })
+		.then((url) => cb(null)(url)())
+		.catch((err) => cb(err)("")());
+};
+
 export const getS3UrlImpl = (cfg, key) => {
 	const endpoint = cfg.endpointUrl || "";
 	const bucket = cfg.bucket || "";
blob - 2139ec602d2398d4f315b9651747202862a8076e
blob + 6bf33fa7e2e92a1716418481a95201958ef6d2e1
--- src/S3.purs
+++ src/S3.purs
@@ -37,6 +37,9 @@ foreign import uploadToS3Impl
 foreign import existsInS3Impl
   :: Fn3 S3ConfigJs String (Nullable Error -> Boolean -> Effect Unit) (Effect Unit)
 
+foreign import getPresignedUrlImpl
+  :: Fn3 S3ConfigJs String (Nullable Error -> String -> Effect Unit) (Effect Unit)
+
 foreign import getS3UrlImpl :: Fn2 S3ConfigJs String String
 
 uploadToS3 :: S3Config -> String -> Buffer -> String -> Aff Unit
@@ -55,5 +58,13 @@ existsInS3 cfg key = makeAff \cb -> do
       Nothing -> cb (Right exists)
   pure nonCanceler
 
+getPresignedUrl :: S3Config -> String -> Aff String
+getPresignedUrl cfg key = makeAff \cb -> do
+  runFn3 getPresignedUrlImpl (toJs cfg) key \err url ->
+    case toMaybe err of
+      Just e -> cb (Left e)
+      Nothing -> cb (Right url)
+  pure nonCanceler
+
 getS3Url :: S3Config -> String -> String
 getS3Url cfg key = runFn2 getS3UrlImpl (toJs cfg) key