Pre-Signed URL

int j =0;Β·2024λ…„ 6μ›” 2일

Pre-Signed URL

πŸ’‘Β S3 λ²„ν‚·μ—μ„œ μ‚¬μš©μžμ—κ²Œ νŒŒμΌμ„ λ°˜ν™˜ν•˜κ±°λ‚˜ μ‚¬μš©μžκ°€ νŒŒμΌμ„ S3 버킷에 μ•ˆμ „ν•˜κ²Œ μ—…λ‘œλ“œ ν•΄μ•Όν•˜λŠ” μƒν™©μ—μ„œ 주둜 μ‚¬μš©

I. Pre-Signed URL μ΄λž€?

βœ…Β μ‚¬μš©μžμ—κ²Œ 미리 μ„œλͺ…λœ URL을 μ œκ³΅ν•˜μ—¬ 이λ₯Ό μ΄μš©ν•˜μ—¬ S3 버킷에 μ ‘κ·Ό κΆŒν•œμ„ μ„€μ •ν•΄ 쀄 수 있음

βœ…Β μ΄λ•Œ, 미리 μ„œλͺ…λœ URL은 URL을 μ•„λŠ” λͺ¨λ“  μ‚¬λžŒμ—κ²Œ Amazon S3 버킷에 λŒ€ν•œ μ•‘μ„ΈμŠ€ κΆŒν•œμ„ λΆ€μ—¬ν•˜λ―€λ‘œ μ μ ˆν•˜κ²Œ μ ‘κ·Ό κΆŒν•œ 및 만료 μ‹œκ°„μ„ μ„€μ •ν•΄μ£Όμ–΄μ•Ό ν•œλ‹€.

  • λͺ¨λ“  κ°μ²΄λŠ” 기본적으둜 λΉ„κ³΅κ°œ
  • μ†Œμœ μžλ§Œ μ•‘μ„ΈμŠ€ κ°€λŠ₯ν•˜λ„λ‘ κΆŒν•œ μ„€μ •
  • λ³΄μ•ˆ 자격 증λͺ…을 μ΄μš©ν•΄ 미리 μ„œλͺ…λœ URL을 μƒμ„±ν•˜μ—¬ 개체λ₯Ό λ‹€μš΄λ‘œλ“œ ν•  수 μžˆλŠ” μ‹œκ°„ μ œν•œμ„ λΆ€μ—¬ β†’ μ„ νƒμ μœΌλ‘œ λ‹€λ₯Έ μ‚¬λžŒκ³Ό 개체λ₯Ό κ³΅μœ ν•¨
πŸ’‘ λΈŒλΌμš°μ €: λ‚˜ 이미지 올릴래. 올릴 수 μžˆλŠ” URL 쀘봐 μ„œλ²„: γ…‡γ…‹ 기달 μ„œλ²„: μ•Ό presigned url 쀘봐. aws: 자. μ„œλ²„: γ„±γ……. λΈŒλΌμš°μ €μ•Ό μ—¬κΈ° μžˆλ‹€. λΈŒλΌμš°μ €: μ—…λ‘œλ“œ μ™„λ£Œ!

ν•„μš”ν•œ 상황

  1. κ°€μ •: S3 λ²„ν‚·μ—μ„œ 일뢀 νŒŒμΌμ„ ν˜ΈμŠ€νŒ…ν•˜κ³  이λ₯Ό μ‚¬μš©μžμ—κ²Œ λ…ΈμΆœν•΄μ•Ό 함
    1. 버킷을 μ˜€ν”ˆλœ μƒνƒœλ‘œ μ„€μ •ν•˜κ³  μ‹Άμ§€ μ•ŠμŒ.
    2. μ΄λŸ¬ν•œ νŒŒμΌμ— λŒ€ν•œ μ•‘μ„ΈμŠ€λ₯Ό 일뢀 μ œμ–΄ν•˜κ³  μ‹ΆμŒ.
      1. μ‚¬μš©μžκ°€ νŒŒμΌμ— μ•‘μ„ΈμŠ€ ν•  수 μžˆλŠ” μ‹œκ°„μ„ μ œν•œ
  2. ν•΄κ²°: 미리 μ„œλͺ…λœ URL을 톡해 μ‚¬μš©μžκ°€ λ¬Έμ„œλ₯Ό μ—…λ‘œλ“œ ν•˜κ±°λ‚˜ μ›ν•˜λŠ” νŒŒμΌμ„ S3 버킷에 μ €μž₯ν•˜λ„λ‘ λ§Œλ“€ 수 있음

πŸ’‘Β μ§€κΈˆ ν•΄λ‹Ή μ„œλΉ„μŠ€μ— ν•„μš”ν•œ 이유?

  • 기쑴의 ν”„λ‘ νŠΈμ—μ„œ 이미지 보내주면 nodeμ—μ„œ multer둜 μ²˜λ¦¬ν•˜μ—¬ 이미지λ₯Ό λ°›μ•„μ™€μ„œ S3 버킷에 μ €μž₯ν•˜λŠ” λ°©μ‹μ˜ λ°œμ „
    • μ„œλ²„μ— 무리?
    • 이후 μ„œλΉ„μŠ€κ°€ 더 μ»€μ‘Œμ„ λ•Œ, 이미지 뿐만 μ•„λ‹ˆλΌ 결과물인 λ¬Έμ„œ 파일의 μš©λŸ‰μ΄ μ»€μ§€κ²Œ λœλ‹€λ©΄?
  • λ³΄μ•ˆ

절차

  1. 자격증λͺ… μ‚¬μš©: URL을 생성할 λ•Œ, AWS의 자격증λͺ…(예: μ•‘μ„ΈμŠ€ ν‚€ ID와 λΉ„λ°€ μ•‘μ„ΈμŠ€ ν‚€)을 μ‚¬μš©ν•˜μ—¬ μš”μ²­μ„ μ„œλͺ…ν•©λ‹ˆλ‹€. 이 μ„œλͺ…은 μš”μ²­μ΄ AWS κ³„μ •μ˜ μ†Œμœ μžλ‘œλΆ€ν„° μ™”μŒμ„ 증λͺ…ν•©λ‹ˆλ‹€.
  2. signedHeaders 포함: μ„œλͺ…을 생성할 λ•Œ, μ„ νƒμ μœΌλ‘œ signedHeaders λ§€κ°œλ³€μˆ˜λ₯Ό ν¬ν•¨μ‹œμΌœ νŠΉμ • HTTP 헀더λ₯Ό μš”μ²­μ˜ μΌλΆ€λ‘œ μ§€μ •ν•©λ‹ˆλ‹€. μ΄λŠ” μ„œλͺ…μ˜ 일뢀가 되며, μš”μ²­ 검증 μ‹œ ν™•μΈλ©λ‹ˆλ‹€.
  3. μ„œλͺ… 및 만료 검증: μš”μ²­μ΄ S3에 λ„μ°©ν•˜λ©΄, S3λŠ” 제곡된 μ„œλͺ…을 κ²€μ¦ν•˜κ³ , URL의 만료 μ‹œκ°„μ΄ 아직 μ§€λ‚˜μ§€ μ•Šμ•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. μ„œλͺ…이 μœ νš¨ν•˜κ³  URL이 아직 λ§Œλ£Œλ˜μ§€ μ•Šμ•˜λ‹€λ©΄, μš”μ²­μ€ μŠΉμΈλ©λ‹ˆλ‹€.

이상적인 μ„€μ •

βœ…Β νŠΉμ • λ¦¬μ†ŒμŠ€μ— λŒ€ν•΄ 미리 μ„œλͺ…λœ URL을 μƒμ„±ν•˜μ—¬, 이λ₯Ό ν΄λΌμ΄μ–ΈνŠΈλ‘œ λ°˜ν™˜ν•˜κΈ° μœ„ν•œ μ œν•œλœ 자격증λͺ…이 μžˆλŠ” μ „μš© λ°±μ—”λ“œ μ„œλΉ„μŠ€λ₯Ό 보유

II. νŠΉμ§•

λŒ€λΆ€λΆ„ AWS의 SDK κΈ°λŠ₯을 μ‚¬μš©ν•˜μ—¬ ꡬ성, 생성 λœλ‹€.

βœ…Β S3κ°€ 선택적 signedHeaders λ§€κ°œλ³€μˆ˜λ₯Ό 계산에 ν¬ν•¨ν•˜κ³  μ„œλͺ…이 μœ νš¨ν•œμ§€, 링크가 아직 λ§Œλ£Œλ˜μ§€ μ•Šμ•˜λŠ”μ§€ ν™•μΈν•˜λŠ” 것을 ν¬ν•¨ν•˜μ—¬ μ§€μ •λœ 자격증λͺ…에 λŒ€ν•΄ λ™μΌν•œ μ„œλͺ…을 κ³„μ‚°ν•˜λ €κ³  μ‹œλ„ν•œλ‹€λŠ” 것.

III. ν”„λ‘œμ νŠΈμ— μ μš©μ‹œμΌœλ³΄μž

1) pre-signed url vs pre-signed post?

μ²˜μŒμ—λŠ” getμš”μ²­μ΄ λ§žλ‹€κ³  νŒλ‹¨ν•˜μ—¬ getμš”μ²­μœΌλ‘œ μ½”λ“œλ₯Ό μž‘μ„±ν–ˆλ‹€. 그리고 μ˜ˆμ‹œλ‘œ μ°Ύμ•„λ³Έ μ½”λ“œ λͺ¨λ‘ presigned url을 get으둜 μž‘μ„±ν–ˆκΈ° λ•Œλ¬Έ.

ν•˜μ§€λ§Œ, ν΄λΌμ΄μ–ΈνŠΈ λ‹¨μ—μ„œ μ΅œλŒ€ 10개의 파일λͺ…을 λ³΄λ‚΄μ£Όμ–΄μ•Όν–ˆκΈ° λ•Œλ¬Έμ—, query둜 ν•˜κΈ° μ–΄λ €μšΈ 것 κ°™μ•˜λ‹€. κ·Έλž˜μ„œ κ²€μƒ‰ν•΄λ³΄λ‹ˆ, presigned postλΌλŠ” 것이 μžˆμ—ˆλ‹€. gptμ—κ²Œ μžμ„Ένžˆ λ¬Όμ–΄λ³΄μž!


μš”μ•½ν•˜μžλ©΄ presigned url은 λ‹€μš΄λ‘œλ“œμ— μ‚¬μš©λ˜κ³ , presigned postλŠ” μ—…λ‘œλ“œμ— μ‚¬μš©λœλ‹€λŠ” 것!

2) code

1️⃣ controller

const express = require('express');
const router = express.Router();
const {
    generateUploadPresignedUrls,
    generateDownDelPresignedUrls,
    parseFileType,
    parseFileName,
} = require('../utils/presigned_file');
const env = process.env;
objectKeys = [];

// ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ putObject presignedURL을 λ°˜ν™˜
async function postPresigned(req, res, next) {
    const { files, inspection_area_id, size, label } = req.body;

    const fileType = parseFileType(files);
    const bucketName = env.BUCKET;

    console.log(files, fileType, inspection_area_id);

    try {
        // μ—…λ‘œλ“œλ₯Ό μœ„ν•œ presignedURL 생성
        const presignedUrl = await generateUploadPresignedUrls(
            files,
            fileType,
            bucketName,
            'putObject'
        );

        objectKeys = presignedUrl.map((item) => item.key);

        res.status(200).send({
            data: { presignedUrl },
        });

        console.log(objectKeys);
    } catch (error) {
        console.error('Failed to generate presigned URL', error);
        res.status(500).send('Failed to generate presigned URL');
    }
}

module.exports.postPresigned = postPresigned;

// ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ getObject presignedURL을 λ°˜ν™˜
async function getPresigned(req, res, next) {
    const { files, inspection_area_id, size, label } = req.body;
    //body둜 파일λͺ…κ³Ό MIME type
    const fileType = parseFileType(files); //db 쑰회
    const bucketName = env.BUCKET;
    console.log(files, fileType, inspection_area_id);

    try {
        const presignedUrl = await generateDownDelPresignedUrls(
            objectKeys,
            fileType,
            bucketName,
            'getObject'
        );
        res.status(200).send({
            data: { presignedUrl },
        });
        console.log('Success to generate presigned URL');
    } catch (error) {
        console.error('Failed to generate presigned URL', error);
        res.status(500).send('Failed to generate presigned URL');
    }
}

module.exports.getPresigned = getPresigned;

// ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ deleteObject presignedURL을 λ°˜ν™˜
async function deletePresigned(req, res, next) {
    const { files, inspection_area_id, size, label } = req.body;
    //body둜 파일λͺ…κ³Ό MIME type
    console.log(objectKeys);
    const fileType = parseFileType(files); //db 쑰회
    const bucketName = env.BUCKET;

    try {
        // μ—…λ‘œλ“œλ₯Ό μœ„ν•œ presignedURL 생성
        const presignedUrl = await generateDownDelPresignedUrls(
            objectKeys,
            fileType,
            bucketName,
            'deleteObject'
        );
        res.status(200).send({
            data: { presignedUrl },
        });
        console.log('Success to generate presigned URL');
    } catch (error) {
        console.error('Failed to generate presigned URL', error);
        res.status(500).send('Failed to generate presigned URL');
    }
}

module.exports.deletePresigned = deletePresigned;

2️⃣ ν•¨μˆ˜

const {
    S3Client,
    GetObjectCommand,
    PutObjectCommand,
    DeleteObjectCommand,
} = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const env = process.env;

// AWS 정보
const s3Client = new S3Client({
    region: 'ap-northeast-2',
    credentials: {
        accessKeyId: env.AWS_ACCESS_KEY_ID,
        secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
    },
});

function parseFileName(files) {
    return files.map((file) => {
        const dotIndex = file.lastIndexOf('.');
        const fileName = file.substring(0, dotIndex);
        return fileName;
    });
}
module.exports.parseFileName = parseFileName;

function parseFileType(files) {
    return files.map((file) => {
        const dotIndex = file.lastIndexOf('.');
        const fileType = file.substring(dotIndex);

        switch (fileType) {
            case '.pdf':
                return 'application/pdf';
            case '.docx':
                return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
            case '.xlsx':
                return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
            case '.hwp':
                return 'application/hwp';
            case '.txt':
                return 'text/plain';

            case '.zip':
                return 'application/zip';

            case '.mp4':
                return 'video/mp4';

            case '.heif':
                return 'image/heif';
            case '.jpeg':
                return 'image/jpeg';
            case '.png':
                return 'image/png ';

            default:
                return 'application/octet-stream'; // κΈ°λ³Έκ°’μœΌλ‘œ, μ•Œλ €μ§€μ§€ μ•Šμ€ 파일 νƒ€μž… 처리
        }
    });
}
module.exports.parseFileType = parseFileType;

// μ—¬λŸ¬κ°œμ˜ pre-signed url 생성
async function generateUploadPresignedUrls(files, fileTypes, bucketName, operation) {
    return Promise.all(
        files.map(async (file, index) => {
            const fileType = fileTypes[index];
            const objectKey = `presigned/${file.substring(
                0,
                file.indexOf('.')
            )}_${Date.now()}.${file.substring(file.indexOf('.') + 1)}`;

            // κ°œλ³„ presignedurl을 μƒμ„±ν•˜κΈ° μœ„ν•œ ν•¨μˆ˜ 호좜
            const presignedUrl = await generatePresignedUrl(
                bucketName,
                fileType,
                objectKey,
                operation
            );
            return {
                url: presignedUrl,
                bucket: bucketName,
                key: objectKey,
            };
        })
    );
}

async function generateDownDelPresignedUrls(
    objectKeys,
    fileTypes,
    bucketName,
    operation
) {
    return Promise.all(
        objectKeys.map(async (objectKey, index) => {
            const fileType = fileTypes[index];
            // κ°œλ³„ presignedurl을 μƒμ„±ν•˜κΈ° μœ„ν•œ ν•¨μˆ˜ 호좜
            const presignedUrl = await generatePresignedUrl(
                bucketName,
                fileType,
                objectKey,
                operation
            );
            return {
                url: presignedUrl,
                bucket: bucketName,
                key: objectKey,
            };
        })
    );
}

// κ°œλ³„ νŒŒμΌμ— λŒ€ν•œ pre-signed url 생성 (put, get, delete)
async function generatePresignedUrl(bucketName, fileType, objectKey, operation) {
    let command;
    // μ—…λ‘œλ“œλ₯Ό μœ„ν•œ url 생성 μ‹œ, command === putObject
    if (operation === 'putObject') {
        command = new PutObjectCommand({
            Bucket: bucketName,
            Key: objectKey,
            ContentType: fileType,
        });
        // λ‹€μš΄λ‘œλ“œλ₯Ό μœ„ν•œ url 생성 μ‹œ, command === getObject
    } else if (operation === 'getObject') {
        command = new GetObjectCommand({
            Bucket: bucketName,
            Key: objectKey,
            ContentType: fileType,
        });
        // μ‚­μ œλ₯Ό μœ„ν•œ url 생성 μ‹œ, command === delteObject
    } else if (operation === 'deleteObject') {
        command = new DeleteObjectCommand({
            Bucket: bucketName,
            Key: objectKey,
            ContentType: fileType,
        });
    }

    try {
        // μœ νš¨μ‹œκ°„ 1μ‹œκ°„ μ„€μ • (60*60)
        const url = await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 });
        return url;
    } catch (err) {
        console.error('Error generating presigned URL', err);
        throw err;
    }
}

module.exports.generateUploadPresignedUrls = generateUploadPresignedUrls;

module.exports.generateDownDelPresignedUrls = generateDownDelPresignedUrls;
profile
뭐든 ν•  수 μžˆλŠ” μ‚¬λžŒ

0개의 λŒ“κΈ€