Skip to content
Snippets Groups Projects
Commit 1784abbc authored by mcs22's avatar mcs22
Browse files

colocado s3

parent 58eeb556
No related branches found
No related tags found
1 merge request!47Issue #57: ADD S3 connection to make simple CRUD
...@@ -10,3 +10,9 @@ DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME} ...@@ -10,3 +10,9 @@ DB_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
# secret # secret
APP_SECRET=0crebI8PUWwj9E+GRzIGpBt64MwY9ufkBzBkYSH2v+Y= APP_SECRET=0crebI8PUWwj9E+GRzIGpBt64MwY9ufkBzBkYSH2v+Y=
#s3
AWS_ACCESS_KEY_ID=ACCESS_KEY
AWS_SECRET_ACCESS_KEY=SECRET_KEY
AWS_REGION=default
S3_BUCKET=BUCKET_NAME
2 0 → 100644
File added
bucket=mecred
acl=public-read
endpoint=https://s3.c3sl.ufpr.br
objects=$(aws --endpoint-url=${endpoint} s3api list-objects --bucket ${bucket} |\
grep -i key | cut -d':' -f2 | sed 's/,//' | sed 's/"//g')
for object in ${objects}; do
aws --endpoint-url=${endpoint} s3api put-object-acl --bucket ${bucket} --key ${object} --acl ${acl};
done;
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"route:test": "bun db:seed && bun test" "route:test": "bun db:seed && bun test"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@hono/swagger-ui": "^0.5.0", "@hono/swagger-ui": "^0.5.0",
"@hono/zod-openapi": "^0.18.3", "@hono/zod-openapi": "^0.18.3",
"@hono/zod-validator": "^0.2.2", "@hono/zod-validator": "^0.2.2",
...@@ -16,6 +17,7 @@ ...@@ -16,6 +17,7 @@
"dotenv-expand": "^11.0.6", "dotenv-expand": "^11.0.6",
"drizzle-orm": "^0.31.2", "drizzle-orm": "^0.31.2",
"drizzle-zod": "^0.5.1", "drizzle-zod": "^0.5.1",
"file-type": "^20.1.0",
"hono": "^4.4.8", "hono": "^4.4.8",
"postgres": "^3.4.4", "postgres": "^3.4.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
......
This diff is collapsed.
...@@ -21,6 +21,10 @@ const EnvSchema = z.object({ ...@@ -21,6 +21,10 @@ const EnvSchema = z.object({
DB_MIGRATING: stringBoolean, DB_MIGRATING: stringBoolean,
DB_SEEDING: stringBoolean, DB_SEEDING: stringBoolean,
APP_SECRET: z.string(), APP_SECRET: z.string(),
AWS_ACCESS_KEY_ID: z.string(),
AWS_SECRET_ACCESS_KEY: z.string(),
AWS_REGION: z.string(),
S3_BUCKET: z.string(),
}) })
export type EnvSchema = z.infer<typeof EnvSchema> export type EnvSchema = z.infer<typeof EnvSchema>
......
...@@ -43,6 +43,7 @@ import { OpenAPIHono } from "@hono/zod-openapi"; ...@@ -43,6 +43,7 @@ import { OpenAPIHono } from "@hono/zod-openapi";
import { commentsRouter, publicCommentsRoute } from './routes/comments.route' import { commentsRouter, publicCommentsRoute } from './routes/comments.route'
import { publicCommentsReplyRoute, commentReplyRouter } from './routes/comment-reply.route' import { publicCommentsReplyRoute, commentReplyRouter } from './routes/comment-reply.route'
import { publicUserCollectionsRoutes, userCollectionsRoute } from './routes/user-collection.route' import { publicUserCollectionsRoutes, userCollectionsRoute } from './routes/user-collection.route'
import { s3Routes } from './routes/s3.route'
...@@ -155,6 +156,7 @@ app ...@@ -155,6 +156,7 @@ app
.route('/comments', commentsRouter) .route('/comments', commentsRouter)
.route('/replyComment', commentReplyRouter) .route('/replyComment', commentReplyRouter)
.route('/userCollections', userCollectionsRoute) .route('/userCollections', userCollectionsRoute)
.route('/s3', s3Routes)
export default app export default app
export type AppType = typeof app export type AppType = typeof app
......
import { deleteObject, getFile, putFile } from "@/services/s3.service";
import { Hono } from "hono";
export const s3Routes = new Hono()
//rota de enviar blob para o s3
//id_ resource é o id do recurso
//faz o upload do conteudo do recurso
.post('/upload/resource/:id_resource',
async (ctx) => {
const id = ctx.req.param('id_resource');
const formData = await ctx.req.formData();
const fileBlob = formData.get('file');
if (!fileBlob || !(fileBlob instanceof Blob)) {
return ctx.json({ message: 'No files sent or invalid format' }, 400)
}
// Obtém o tamanho do arquivo em bytes
const fileSize = fileBlob.size;
const maxSize = 5 * 1024 * 1024 * 1024; // 5GB em bytes
if (fileSize > maxSize) {
return ctx.json({
message: `O arquivo excede o limite de 5GB (${(fileSize / (1024 * 1024 * 1024)).toFixed(2)} GB).`,
status: 'error'
}, 400);
}
try {
const result = await putFile(fileBlob, id, "resource")
return ctx.json({
message: 'Arquivo enviado com sucesso!',
fileId: id,
status: 'success',
details: result
}, 200);
} catch (error) {
console.error('Erro ao fazer upload do arquivo:', error);
return ctx.json({
message: 'Erro interno ao enviar o arquivo. Tente novamente mais tarde.',
status: 'error'
}, 500);
}
}
)
// envio de fotos de thumbnail do recurso
.post('/upload/thumbnail/resource/:id_resource',
async (ctx) => {
const id = ctx.req.param('id_resource');
const formData = await ctx.req.formData();
const fileBlob = formData.get('file');
if (!fileBlob || !(fileBlob instanceof Blob)) {
return ctx.json({ message: 'No files sent or invalid format' }, 400)
}
// Obtém o tamanho do arquivo em bytes
const fileSize = fileBlob.size;
const maxSize = 1 * 1024 * 1024; // 1MB em bytes
if (fileSize > maxSize) {
return ctx.json({
message: `A thumbnail do recurso excede o limite de 1MB (${(fileSize / (1024)).toFixed(2)} KB).`,
status: 'error'
}, 400);
}
try {
const result = await putFile(fileBlob, id, "thumbnail/resource")
return ctx.json({
message: 'Arquivo enviado com sucesso!',
fileId: id,
status: 'success',
details: result
}, 200);
} catch (error) {
console.error('Erro ao fazer upload do arquivo:', error);
return ctx.json({
message: 'Erro interno ao enviar o arquivo. Tente novamente mais tarde.',
status: 'error'
}, 500);
}
}
)
// envio de fotos de thumbnail da coleção
.post('/upload/thumbnail/collection/:id_collection',
async (ctx) => {
const id = ctx.req.param('id_collection');
const formData = await ctx.req.formData();
const fileBlob = formData.get('file');
if (!fileBlob || !(fileBlob instanceof Blob)) {
return ctx.json({ message: 'No files sent or invalid format' }, 400)
}
// Obtém o tamanho do arquivo em bytes
const fileSize = fileBlob.size;
const maxSize = 1 * 1024 * 1024; // 1MB em bytes
if (fileSize > maxSize) {
return ctx.json({
message: `A thumbnail da collection excede o limite de 1MB (${(fileSize / (1024)).toFixed(2)} KB).`,
status: 'error'
}, 400);
}
try {
const result = await putFile(fileBlob, id, "thumbnail/collection")
return ctx.json({
message: 'Arquivo enviado com sucesso!',
fileId: id,
status: 'success',
details: result
}, 200);
} catch (error) {
console.error('Erro ao fazer upload do arquivo:', error);
return ctx.json({
message: 'Erro interno ao enviar o arquivo. Tente novamente mais tarde.',
status: 'error'
}, 500);
}
}
)
// envio de fotos de thumbnail da coleção
.post('/upload/avatar/:id_avatar',
async (ctx) => {
const id = ctx.req.param('id_avatar');
const formData = await ctx.req.formData();
const fileBlob = formData.get('file');
if (!fileBlob || !(fileBlob instanceof Blob)) {
return ctx.json({ message: 'No files sent or invalid format' }, 400)
}
// Obtém o tamanho do arquivo em bytes
const fileSize = fileBlob.size;
const maxSize = 5 * 1024 * 1024; // 5MB em bytes
if (fileSize > maxSize) {
return ctx.json({
message: `O avatar excede o limite de 5MB (${(fileSize / (1024)).toFixed(2)} KB).`,
status: 'error'
}, 400);
}
try {
const result = await putFile(fileBlob, id, "avatar")
return ctx.json({
message: 'Arquivo enviado com sucesso!',
fileId: id,
status: 'success',
details: result
}, 200);
} catch (error) {
console.error('Erro ao fazer upload do arquivo:', error);
return ctx.json({
message: 'Erro interno ao enviar o arquivo. Tente novamente mais tarde.',
status: 'error'
}, 500);
}
}
)
.post("/delete/resource/:id", async (c) => {
const id = c.req.param("id");
try {
const response = await deleteObject(`resource/${id}`);
return c.json(response);
} catch (error) {
return c.json({ error: "Erro ao deletar objeto do S3." }, 500);
}
})
.post("/delete/avatar/:id", async (c) => {
const id = c.req.param("id");
try {
const response = await deleteObject(`avatar/${id}`);
return c.json(response);
} catch (error) {
return c.json({ error: "Erro ao deletar objeto do S3." }, 500);
}
})
.post("/delete/thumbnail/resource/:id", async (c) => {
const id = c.req.param("id");
try {
const response = await deleteObject(`thumbnail/resource/${id}`);
return c.json(response);
} catch (error) {
return c.json({ error: "Erro ao deletar objeto do S3." }, 500);
}
})
.post("/delete/thumbnail/collection/:id", async (c) => {
const id = c.req.param("id");
try {
const response = await deleteObject(`thumbnail/collection/${id}`);
return c.json(response);
} catch (error) {
return c.json({ error: "Erro ao deletar objeto do S3." }, 500);
}
})
.get("get/resource/:id",
async (ctx) => {
const id = ctx.req.param("id");
try {
// Busca o arquivo no S3 com a chave "resource/id"
const response = await getFile(`resource/${id}`);
if (!response || !response.Body) {
return ctx.json({ message: "Arquivo não encontrado" }, 404);
}
// O corpo da resposta é o arquivo, e seu tipo de conteúdo pode ser acessado diretamente
const fileData = await response.Body.transformToByteArray()// Converte o conteúdo do corpo para ArrayBuffer ou outro formato adequado
return new Response(fileData, {
status: 200,
headers: {
"Content-Type": response.ContentType || "application/octet-stream", // Obtém o tipo de conteúdo diretamente da resposta
}
});
} catch (error) {
console.error("Erro ao buscar arquivo do S3:", error);
return ctx.json(
{ message: "Erro interno ao buscar o arquivo" },
500
);
}
}
)
.get("get/thumbnail/resource/:id",
async (ctx) => {
const id = ctx.req.param("id");
try {
// Busca o arquivo no S3 com a chave "resource/id"
const response = await getFile(`thumbnail/resource/${id}`);
if (!response || !response.Body) {
return ctx.json({ message: "Arquivo não encontrado" }, 404);
}
// O corpo da resposta é o arquivo, e seu tipo de conteúdo pode ser acessado diretamente
const fileData = await response.Body.transformToByteArray()// Converte o conteúdo do corpo para ArrayBuffer ou outro formato adequado
return new Response(fileData, {
status: 200,
headers: {
"Content-Type": response.ContentType || "application/octet-stream", // Obtém o tipo de conteúdo diretamente da resposta
}
});
} catch (error) {
console.error("Erro ao buscar arquivo do S3:", error);
return ctx.json(
{ message: "Erro interno ao buscar o arquivo" },
500
);
}
}
)
.get("get/thumbnail/collection/:id",
async (ctx) => {
const id = ctx.req.param("id");
try {
// Busca o arquivo no S3 com a chave "resource/id"
const response = await getFile(`thumbnail/collection/${id}`);
if (!response || !response.Body) {
return ctx.json({ message: "Arquivo não encontrado" }, 404);
}
// O corpo da resposta é o arquivo, e seu tipo de conteúdo pode ser acessado diretamente
const fileData = await response.Body.transformToByteArray()// Converte o conteúdo do corpo para ArrayBuffer ou outro formato adequado
return new Response(fileData, {
status: 200,
headers: {
"Content-Type": response.ContentType || "application/octet-stream", // Obtém o tipo de conteúdo diretamente da resposta
}
});
} catch (error) {
console.error("Erro ao buscar arquivo do S3:", error);
return ctx.json(
{ message: "Erro interno ao buscar o arquivo" },
500
);
}
}
)
.get("get/avatar/:id",
async (ctx) => {
const id = ctx.req.param("id");
try {
// Busca o arquivo no S3 com a chave "resource/id"
const response = await getFile(`avatar/${id}`);
if (!response || !response.Body) {
return ctx.json({ message: "Arquivo não encontrado" }, 404);
}
// O corpo da resposta é o arquivo, e seu tipo de conteúdo pode ser acessado diretamente
const fileData = await response.Body.transformToByteArray()// Converte o conteúdo do corpo para ArrayBuffer ou outro formato adequado
return new Response(fileData, {
status: 200,
headers: {
"Content-Type": response.ContentType || "application/octet-stream", // Obtém o tipo de conteúdo diretamente da resposta
}
});
} catch (error) {
console.error("Erro ao buscar arquivo do S3:", error);
return ctx.json(
{ message: "Erro interno ao buscar o arquivo" },
500
);
}
}
)
var mailConfig: any; var mailConfig: any;
mailConfig = { mailConfig = {
......
import env from '../env.ts';
import { S3Client, PutObjectCommand, S3ServiceException, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
import { Buffer } from 'buffer';
import { fileTypeFromBuffer } from "file-type";
// cria um cliente s3
const s3Client = new S3Client({
endpoint: 'https://s3.c3sl.ufpr.br',
region: env.AWS_REGION,
credentials: {
accessKeyId: env.AWS_ACCESS_KEY_ID,
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
},
});
//função de upload de arquivos para o s3, id é o id do objeto
// file is Blob-like, é tipo um Blob mas não é
// usamos pick para construir um tipo
export async function putFile(file: { arrayBuffer(): Promise<ArrayBuffer> }, id: string, path: string) {
const buffer = (await file.arrayBuffer());
const fileType = await fileTypeFromBuffer(buffer); // Detecta o tipo MIME real
const command = new PutObjectCommand({
Bucket: env.S3_BUCKET,
Key: `${path}/${id}`, // Key é uma string, criando as "subpastas"
Body: Buffer.from(buffer),
ContentType: fileType?.mime || "application/octet-stream", //manda o MIME, para que ele consiga fazer tudo certo
ACL: "public-read" //permissao de leitura no objeto
})
try {
const response = await s3Client.send(command);
console.log(response)
return response
} catch (caught) {
if (
caught instanceof S3ServiceException &&
caught.name === "EntityTooLarge"
) {
console.error(
`Error from S3 while uploading object to ${env.S3_BUCKET}. \
The object was too large .`,
);
} else if (caught instanceof S3ServiceException) {
console.error(
`Error from S3 while uploading object to ${env.S3_BUCKET}. ${caught.name}: ${caught.message}`,
);
} else {
console.error(
`Error is not from S3..`, caught
);
}
}
}
const checkObjectExists = async ( key: string) => {
try {
await s3Client.send(new HeadObjectCommand({ Bucket: env.S3_BUCKET, Key: key }));
return true; // O objeto existe
} catch (error) {
if (error instanceof S3ServiceException && error.name === "NotFound") {
return false; // O objeto não existe
}
throw error; // Outro erro inesperado
}
};
export async function deleteObject(id: string) {
const command = new DeleteObjectCommand({
Bucket: env.S3_BUCKET,
Key: id
})
const exists = await checkObjectExists(id);
if (!exists) {
return { error: `Objeto ${id} não encontrado.`, status: 404 }; // Retorna um JSON se não existir
}
try {
await s3Client.send(command);
console.log(`The object "${id}" from bucket "${env.S3_BUCKET}" was deleted, or it didn't exist.`);
return { message: `The object "${id}" was deleted successfully.` };
} catch (caught) {
if (caught instanceof S3ServiceException && caught.name === "NoSuchBucket") {
console.error(`Error from S3 while deleting object from ${env.S3_BUCKET}. The bucket doesn't exist.`);
throw new Error("Bucket does not exist.");
} else if (caught instanceof S3ServiceException) {
console.error(`Error from S3 while deleting object from ${env.S3_BUCKET}. ${caught.name}: ${caught.message}`);
throw new Error(`S3 Error: ${caught.name}: ${caught.message}`);
} else {
throw caught;
}
}
}
export async function getFile(id: string) {
const command = new GetObjectCommand({
Bucket: env.S3_BUCKET,
Key: id
})
try {
const response = await s3Client.send(command);
if (!response.Body) {
throw new Error("File not found in S3");
}
return response;
} catch (error) {
console.error("Error to get file in S3:", error);
if (error instanceof Error) {
console.error("Nome do erro:", error.name);
console.error("Mensagem:", error.message);
console.error("Stack trace:", error.stack);
}
throw error;
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment