Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add minio arch & user avatar upload #7

Merged
merged 10 commits into from
Nov 4, 2024
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ POSTGRES_HOST=postgres
REDIS_HOST=redis

MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=minio
MINIO_ROOT_PASSWORD=password
MINIO_HOST=minio
MINIO_DEFAULT_BUCKETS=george

Expand Down Expand Up @@ -49,4 +49,4 @@ MAIL_REDIRECT_URL="http://localhost:1337/auth/confirm#{token}"

# Profile redirect URL
# The {uuid} placeholder will be replaced by the user's UUID
PROFILE_REDIRECT_URL="http://localhost:1337/profile/{uuid}"
PROFILE_REDIRECT_URL="/profile/{uuid}"
2 changes: 1 addition & 1 deletion controllers/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default abstract class UserController {
}
}

public static async updateUser(uuid: string, username?: string, avatarUrl?: string, quote?: string) {
public static async updateUser(uuid: string, username?: string, avatarUrl?: string | null, quote?: string) {
try {
const user = await DB.instance
.update(users)
Expand Down
2 changes: 2 additions & 0 deletions database/init.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { initDrizzle } from "./config";
import Redis from "./redis";
import S3 from "./s3";

export function initDatabase() {
initDrizzle();
Redis.init();
S3.init();

return () => {
Redis.close();
Expand Down
93 changes: 93 additions & 0 deletions database/s3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import globals from "@/env/env";
import Logger from "@/log/logger";
import compress from "@/utils/compress";
import { randomUUID } from "crypto";
import { Client } from "minio";
import internal from "stream";

export default abstract class S3 {
public static client: Client;

public static init() {
S3.client = new Client({
endPoint: globals.env.MINIO_HOST,
port: globals.env.MINIO_PORT,
useSSL: false,
accessKey: globals.env.MINIO_ROOT_USER,
secretKey: globals.env.MINIO_ROOT_PASSWORD,
pathStyle: true
});

S3.client
.bucketExists(globals.env.MINIO_DEFAULT_BUCKETS)
.then(
(exists) =>
!exists &&
Logger.error(`s3.ts::init | Default bucket ${globals.env.MINIO_DEFAULT_BUCKETS} does not exist`)
);
}

/**
* Uploads a compressed image to Minio as a base64 string.
* @param {string} data The base64 string of the image.
* @param {string} options.path The path to upload the image to.
* @param {string} options.creator The creator of the image.
* @returns {Promise<string | null>} The path of the uploaded image.
*/
public static async putImage(data: string, options?: { path?: string; creator?: string }): Promise<string | null> {
try {
const imagePath = options?.path ?? randomUUID();
const image = await compress(data);

await S3.client.putObject(globals.env.MINIO_DEFAULT_BUCKETS, imagePath, image, image.length, {
"Content-Type": "application/octet-stream",
"Last-Modified": new Date().toUTCString(),
"x-amz-acl": "public-read",
"x-amz-meta-creator": options?.creator
});

return imagePath;
} catch (err) {
Logger.error("s3.ts::putImage | Error compressing image", err);
return null;
}
}

/**
* Gets a base64 image.
* @param {string} path
* @returns {Promise<string | null>}
*/
public static async getImage(path: string): Promise<string | null> {
try {
const data: internal.Readable = await S3.client.getObject(globals.env.MINIO_DEFAULT_BUCKETS, path);
if (!data) return null;

const buffer = await new Promise<Buffer>((resolve, reject) => {
const chunks: Buffer[] = [];
data.on("data", (chunk: Buffer) => {
chunks.push(chunk);
})
.on("end", () => {
resolve(Buffer.concat(chunks));
})
.on("error", (err) => {
reject(err);
});
});

return buffer.toString("base64");
} catch (err) {
Logger.error("s3.ts::getImage | Error getting image", err);
return null;
}
}

/**
* @description Deletes an image from the S3 bucket.
* @param {string} path The path of the image to delete.
*/
public static async deleteImage(path: string) {
await S3.client.removeObject(globals.env.MINIO_DEFAULT_BUCKETS, path);
}
}
8 changes: 5 additions & 3 deletions docker/compose.ci.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ services:
REDIS_HOST: redis

MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_ROOT_PASSWORD: password
MINIO_HOST: minio
MINIO_DEFAULT_BUCKETS: george

Expand All @@ -33,6 +33,8 @@ services:
PROFILE_REDIRECT_URL: http://localhost:1337/profile/{uuid}
depends_on:
- postgres
- redis
- minio

postgres:
image: postgres:17-alpine
Expand All @@ -46,8 +48,8 @@ services:
image: redis:7.4

minio:
image: "minio/minio:latest"
image: "bitnami/minio:latest"
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_ROOT_PASSWORD: password
MINIO_DEFAULT_BUCKETS: george
18 changes: 12 additions & 6 deletions docker/compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ services:
ports:
- "0.0.0.0:3000:3000"
volumes:
- /data/app/node_modules
- ..:/data/app
environment:
NODE_ENV: development
Expand All @@ -17,27 +18,32 @@ services:
REDIS_HOST: redis

MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_ROOT_PASSWORD: password
MINIO_HOST: minio
MINIO_DEFAULT_BUCKETS: george
stdin_open: true
tty: true
depends_on:
- postgres
- redis
- minio

redis:
image: redis:7.4
ports:
- "0.0.0.0:6379:6379"
- "0.0.0.0:3001:6379"

minio:
image: minio/minio
image: bitnami/minio
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_ROOT_PASSWORD: password
MINIO_DEFAULT_BUCKETS: george
ports:
- "0.0.0.0:8900:9000"
- "0.0.0.0:3002:9000"
# # Uncomment the next line to have a persistent volume for the Minio server
# volumes:
# - minio:/bitnami/minio/data

postgres:
image: postgres:17-alpine
Expand All @@ -47,7 +53,7 @@ services:
POSTGRES_PASSWORD: postgres
POSTGRES_HOST: postgres
ports:
- "0.0.0.0:3030:5432" # Use port 3030 for external debugging
- "0.0.0.0:3003:5432" # Use port 3030 for external debugging
# # Uncomment the next line to have a persistent volume for the database
# volumes:
# - postgres:/var/lib/postgresql/data
8 changes: 5 additions & 3 deletions docker/compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ services:
REDIS_HOST: redis

MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_ROOT_PASSWORD: password
MINIO_HOST: minio
MINIO_DEFAULT_BUCKETS: george

Expand All @@ -39,6 +39,8 @@ services:
PROFILE_REDIRECT_URL: http://localhost:1337/profile/{uuid}
depends_on:
- postgres
- redis
- minio

postgres:
image: postgres:17-alpine
Expand All @@ -52,8 +54,8 @@ services:
image: redis:7.4

minio:
image: "minio/minio:latest"
image: "bitnami/minio:latest"
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: minio
MINIO_ROOT_PASSWORD: password
MINIO_DEFAULT_BUCKETS: george
4 changes: 3 additions & 1 deletion docker/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ services:
- ../logs:/logs
depends_on:
- postgres
- redis
- minio

postgres:
image: postgres:17-alpine
Expand All @@ -29,7 +31,7 @@ services:
image: redis:7.4

minio:
image: "minio/minio:latest"
image: "bitnami/minio:latest"
env_file:
- path: ../.env
required: true
Expand Down
2 changes: 1 addition & 1 deletion env/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const envSchema = z.object({
REDIS_PORT: znumber().default("6379"),

MINIO_ROOT_USER: z.string().default("minio"),
MINIO_ROOT_PASSWORD: z.string().default("minio"),
MINIO_ROOT_PASSWORD: z.string().default("password"),
MINIO_HOST: z.string().default("minio"),
MINIO_PORT: znumber().default("9000"),
MINIO_DEFAULT_BUCKETS: z.string().default("george"),
Expand Down
4 changes: 4 additions & 0 deletions i18n/en/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"template": "This is a template error message",
"validation": "Validation error",
"database": "Database error",
"image": {
"invalid": "Invalid image",
"notFound": "Image not found"
},
"auth": {
"admin": "Invalid X-ADMIN-KEY header",
"toomany": "Too many requests",
Expand Down
Loading
Loading