9ad43aa8b2
* Add GHCR and Docker Hub image publishing with fully runtime-configurable pull-first Docker setup (#1) * feat: add container image publishing workflow and pull-based compose setup Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/23c31167-9e15-4d44-ab89-f267b8cd6304 * refine docker publishing workflow and pull-first compose docs Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/23c31167-9e15-4d44-ab89-f267b8cd6304 * chore: temporarily expose docker publish run results for pr branch Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/c946a2f2-2219-4dea-a829-61b287bc4859 * fix: update pinned SHAs for setup-qemu-action and setup-buildx-action to v3 heads Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/0db6957e-476b-48e0-acf3-bee6964c3b32 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * chore: switch all workflow action refs from SHA pins to stable version tags Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/19334b37-9b4c-45df-9c3c-81a497cef8e8 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: restore missing `with:` blocks lost during SHA-to-tag substitution Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/f204f742-5b7d-4f05-9647-03b9db86ea3d Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix(ci): checkout submodules for docker image workflow Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/72309e8a-6c7c-4004-902a-565f67e5c15f Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * feat(docker): support runtime client env for pulled web image * refactor(web): serve runtime config via script endpoint * fix(web): escape runtime config script payload * fix: align docker runtime config with internal/public backend urls Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/3bbcb608-6202-4f9e-b288-5c95a259da93 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * chore: align published workflow tags with docs * docs: clarify runtime config precedence and linux host mapping Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/3bbcb608-6202-4f9e-b288-5c95a259da93 * refactor: move storage/quota config from build args to runtime env Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/da9b749e-0b1c-47b9-b474-5009765b6ea6 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: load runtime config in pages router app shell Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/b375132c-8317-4c98-b437-ae48c0153e3d Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: apply biome formatting for runtime config quota helpers Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/778f5d75-884e-401b-b7cb-4ab40bc64a11 Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> Co-authored-by: Amir Pourmand <pourmand1376@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: S3_PUBLIC_ENDPOINT, _document.tsx for beforeInteractive, runtimeConfig fallbacks, README port (#2) Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/4f92f818-c008-4caa-9684-d530500b5fb2 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> * fix: reformat runtimeConfig.ts to satisfy biome formatter (#3) Agent-Logs-Url: https://github.com/pourmand1376/readest/sessions/4eea77f4-67ab-4428-b59e-0b21be988037 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pourmand1376 <32064808+pourmand1376@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
139 lines
4.1 KiB
TypeScript
139 lines
4.1 KiB
TypeScript
import { S3Client } from '@aws-sdk/client-s3';
|
|
import {
|
|
GetObjectCommand,
|
|
DeleteObjectCommand,
|
|
PutObjectCommand,
|
|
HeadObjectCommand,
|
|
CopyObjectCommand,
|
|
} from '@aws-sdk/client-s3';
|
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
|
|
const S3_ENDPOINT = process.env['S3_ENDPOINT'] || '';
|
|
// S3_PUBLIC_ENDPOINT is the MinIO URL reachable by browsers (e.g. http://<host-ip>:9000).
|
|
// When unset it falls back to S3_ENDPOINT so single-endpoint setups are unaffected.
|
|
const S3_PUBLIC_ENDPOINT = process.env['S3_PUBLIC_ENDPOINT'] || S3_ENDPOINT;
|
|
const S3_REGION = process.env['S3_REGION'] || 'auto';
|
|
const S3_ACCESS_KEY_ID = process.env['S3_ACCESS_KEY_ID'] || '';
|
|
const S3_SECRET_ACCESS_KEY = process.env['S3_SECRET_ACCESS_KEY'] || '';
|
|
|
|
const s3ClientCredentials = {
|
|
accessKeyId: S3_ACCESS_KEY_ID,
|
|
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
};
|
|
|
|
// Internal client used for server-side SDK calls (PutObject, CopyObject, etc.)
|
|
export const s3Client = new S3Client({
|
|
forcePathStyle: true,
|
|
region: S3_REGION,
|
|
endpoint: S3_ENDPOINT,
|
|
credentials: s3ClientCredentials,
|
|
});
|
|
|
|
// Signing client uses S3_PUBLIC_ENDPOINT so presigned URLs contain a hostname
|
|
// that browsers can reach (S3_ENDPOINT may be an internal docker hostname like
|
|
// "minio:9000" which is not resolvable outside the docker network).
|
|
const s3SigningClient = new S3Client({
|
|
forcePathStyle: true,
|
|
region: S3_REGION,
|
|
endpoint: S3_PUBLIC_ENDPOINT,
|
|
credentials: s3ClientCredentials,
|
|
});
|
|
|
|
export const s3Storage = {
|
|
getClient: () => {
|
|
return new S3Client({
|
|
forcePathStyle: true,
|
|
region: S3_REGION,
|
|
endpoint: S3_ENDPOINT,
|
|
credentials: s3ClientCredentials,
|
|
});
|
|
},
|
|
|
|
getDownloadSignedUrl: async (bucketName: string, fileKey: string, expiresIn: number) => {
|
|
const getCommand = new GetObjectCommand({
|
|
Bucket: bucketName,
|
|
Key: fileKey,
|
|
});
|
|
const downloadUrl = await getSignedUrl(s3SigningClient, getCommand, {
|
|
expiresIn: expiresIn,
|
|
});
|
|
return downloadUrl;
|
|
},
|
|
|
|
getUploadSignedUrl: async (
|
|
bucketName: string,
|
|
fileKey: string,
|
|
contentLength: number,
|
|
expiresIn: number,
|
|
) => {
|
|
const signableHeaders = new Set<string>();
|
|
signableHeaders.add('content-length');
|
|
const putCommand = new PutObjectCommand({
|
|
Bucket: bucketName,
|
|
Key: fileKey,
|
|
ContentLength: contentLength,
|
|
});
|
|
|
|
const uploadUrl = await getSignedUrl(s3SigningClient, putCommand, {
|
|
expiresIn: expiresIn,
|
|
signableHeaders,
|
|
});
|
|
|
|
return uploadUrl;
|
|
},
|
|
|
|
putObject: async (
|
|
bucketName: string,
|
|
fileKey: string,
|
|
body: ArrayBuffer | string,
|
|
contentType: string,
|
|
) => {
|
|
const putCommand = new PutObjectCommand({
|
|
Bucket: bucketName,
|
|
Key: fileKey,
|
|
Body: body instanceof ArrayBuffer ? new Uint8Array(body) : body,
|
|
ContentType: contentType,
|
|
});
|
|
return await s3Storage.getClient().send(putCommand);
|
|
},
|
|
|
|
deleteObject: async (bucketName: string, fileKey: string) => {
|
|
const deleteCommand = new DeleteObjectCommand({
|
|
Bucket: bucketName,
|
|
Key: fileKey,
|
|
});
|
|
|
|
return await s3Storage.getClient().send(deleteCommand);
|
|
},
|
|
|
|
headObject: async (bucketName: string, fileKey: string) => {
|
|
const headCommand = new HeadObjectCommand({
|
|
Bucket: bucketName,
|
|
Key: fileKey,
|
|
});
|
|
|
|
return await s3Storage.getClient().send(headCommand);
|
|
},
|
|
|
|
copyObject: async (
|
|
bucketName: string,
|
|
sourceFileKey: string,
|
|
destFileKey: string,
|
|
sourceBucketName?: string,
|
|
) => {
|
|
const srcBucket = sourceBucketName || bucketName;
|
|
// S3 requires CopySource to be URL-encoded segment-by-segment. file_key
|
|
// is built from the original filename, so spaces and reserved chars
|
|
// (e.g. `My Book.epub`, `A&B.epub`) are common and would otherwise
|
|
// break the copy.
|
|
const encodeKey = (key: string): string => key.split('/').map(encodeURIComponent).join('/');
|
|
const copyCommand = new CopyObjectCommand({
|
|
Bucket: bucketName,
|
|
Key: destFileKey,
|
|
CopySource: `${srcBucket}/${encodeKey(sourceFileKey)}`,
|
|
});
|
|
|
|
return await s3Storage.getClient().send(copyCommand);
|
|
},
|
|
};
|