×
Community Blog Upload file lên OSS qua Presigned Url

Upload file lên OSS qua Presigned Url

OSS là một dịch vụ lưu trữ được cung cấp bởi Alibaba Cloud, xếp vào loại serverless storage, nghĩa là người dùng chỉ cần quan tâm đến việc lưu trữ fil.

OSS là một dịch vụ lưu trữ được cung cấp bởi Alibaba Cloud, xếp vào loại serverless storage, nghĩa là người dùng chỉ cần quan tâm đến việc lưu trữ file, các vấn đề về mở rộng dung lượng, bảo mật, hạ tầng,... được quản lý bởi nền tảng của alibaba cloud. OSS cung cấp các API hoạt động độc lập với nền tảng, cho phép người dùng tải lên và truy cập dữ liệu của mình từ mọi ứng dụng, mọi lúc và mọi nơi. Có nhiều cách tải upload và download file lên OSS, có thể trực tiếp tải trên giao diện web console, hoặc sử dụng các API, SDK, đặc biệt, OSS cũng tương thích với các API cho S3 nên rất dễ sử dụng. Trong bài viết này, mình thử nghiệm tính năng upload và download file với presigned url của OSS.

1. Presigned Url

Có thể upload file lên oss qua các SDK, khi sử dụng các SDK này cần cung cấp các thông tin xác thực như access key, secret key. Các presigned url cho phép thao tác thông qua các url này mà không cần bất cứ phương thức xác thực nào. Điều này hữu ích khi người dùng muốn tạm thời cấp quyền cho một ứng dụng khác sử dụng các tài nguyên trên OSS mà không cần chia sẻ thông tin xác thực ( ví dụ như cho phép user truy cập vào 1 file private trên oss trong 1 giờ).
Từ lý thuyết trên, khi một ứng dụng (web) cần cho phép người dùng tải file lên để lưu trữ, thay vì file từ browser được post lên server, và server sẽ đọc file đó rồi upload lên OSS, thì với Presigned Url, dev có thể tạo một url tạm thời, cho phép user được upload/download file trực tiếp lên OSS mà không cần thông qua web server. Điều này giúp giảm tải ở phía server và tăng tốc thời gian upload/download. (do server không phải xử lý việc upload/download file).

2. Dựng ứng dụng đơn giản với flask

Mình sẽ sử dụng flask để thử nghiệm tính năng trên, với cấu trúc đơn giản bao gồm 1 file chính, file chứa config và file index.html cho giao diện frontend:

.
└── flask_app
    ├── app.py
    ├── static
    │   ├── styles.css
    ├── templates
    │   ├── index.html
    └── config.py

Với file app.py mình có:

@app.route('/')
def index():
    return render_template('index.html')

Và giao diện cơ bản của mình:
Ali_oss_ui

3. Upload file

Với giao diện mình vừa tạo như trên, về phần upload mình viết một đoạn html đơn giản để lấy file:

<div class="upload-section">
            <input type="file" id="fileInput">
            <button onclick="uploadFile()">Upload</button>
        </div>

Và mình cần triển khai hàm uploadFile(), hàm này sẽ có 2 nhiệm vụ, thứ nhất là gửi thông tin file đến server (file name và content type,...), sau đó, server sẽ dựa vào những thông tin này, tạo một presign url và trả về, lúc này, hàm uploadFile sẽ upload file lên url vừa nhận được.

async function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            const fileName = file.name;
            const contentType = file.type;
            try {
                // get presigned url
                const response = await axios.post('/upload', {
                    fileName: fileName,
                    contentType: contentType
                });
                const uploadUrl = response.data.uploadUrl;
                console.log(uploadUrl)
                await axios.put(uploadUrl, file, {
                    headers: {
                        'x-amz-acl': 'private',
                        'Content-Type': contentType
                    }
                }).catch(function (error) {
                console.log(error);
                });
                alert('Tệp đã được upload thành công!');
                fetchFiles();
            } catch (error) {
                console.error('Status Code:', error);
                console.error('Response Data:', error);
            }
        }

Tất nhiên khi gọi axios.post('/upload',...), thì ở flask server, phải viết một hàm xử lý request này, hàm sẽ đọc thông tin mà trình duyệt gửi đến, sau đó gọi lên OSS để tạo một url và trả về cho user.
Mà trước đó mình cần thiết lập kết nối đến OSS bằng boto3:

import boto3
from botocore.exceptions import NoCredentialsError
from config import BUCKET, ACCESS_KEY, SECRET_KEY, ENDPOINT, REGION
from botocore.config import Config
s3_client = boto3.client(
    's3',
    config=Config(s3={'addressing_style': 'virtual'}),
    endpoint_url='https://oss-ap-southeast-1.aliyuncs.com',
    region_name='oss-ap-southeast-1';
    aws_access_key_id=ACCESS_KEY,
    aws_secret_access_key=SECRET_KEY,
)

Đối với region và endpoint có thể tham khảo: #https://www.alibabacloud.com/help/en/oss/user-guide/regions-and-endpoints
Region_name là giá trị nằm ở cột OSS Region ID, hoặc có thể xem thông tin này trên giao diện web console. Mình cũng gặp lỗi SecondLevelDomainForbidden (Please use virtual hosted style to access) khi gọi các hàm s3_client, nên cần phải thêm config=Config(s3={'addressing_style': 'virtual'}) để tránh lỗi trên.
Tiếp theo, mình xử lý request upload mà browser gửi lên:

@app.route('/upload', methods=['POST'])
def upload():
    file_name = request.json.get('fileName')
    content_type = request.json.get('contentType')
    file_key = f"{file_name}"
    try:
        # Tạo presigned URL cho tác vụ upload
        upload_url = s3_client.generate_presigned_url('put_object',
            Params={'Bucket': BUCKET, 'Key': file_key, 'ACL':'private','ContentType':content_type},
            ExpiresIn=60,
        )
        print(upload_url)
        return jsonify(uploadUrl=upload_url)
    except NoCredentialsError:
        return jsonify({'error': 'Credentials not found'}), 403

Hàm tạo url này có tham số ExpiresIn, thời gian hết hạn của url, nếu qua quãng thời gian này sẽ không thể sử dụng url này được nữa mà phải tạo một url mới. Thời gian nhỏ nhất hợp lệ là 60 giây. Một điều lưu ý nữa là khi url đã được sử dụng, mà thời gian upload file vượt quá expire time, thì file vẫn được upload bình thường (đối với simple upload). Và điều lưu ý thứ 2 là về phía frontend, các tham số truyền vào phải đảm bảo giống nhưa đã gọi ở server, nghĩa là ở server, đoạn code trên truyền vào ACL và ContentType thì tại phía frontend phải tuân theo đủ như vậy nếu không sẽ gặp lỗi (do sai signiture nên bị lỗi 403-Forbidden):

axios.put(uploadUrl, file, {
    headers: {
        'x-amz-acl': 'private',
        'Content-Type': contentType
}

Url trả về có dạng:

https://demo-presigned.oss-ap-southeast-1.aliyuncs.com/topik.csv?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=LTAI5t73CVWciQ23pHHmoMFN%2F20240906%2Foss-ap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20240906T041551Z&X-Amz-Expires=60&X-Amz-SignedHeaders=content-type%3Bhost%3Bx-amz-acl&X-Amz-Signature=f77a65ce4d9c0d517aafedaecee205e7eea46f829df1e1ec863ceba4a44c6c31

Khi upload thành công, mình có danh sách các object trả về:
Ali_oss_upload

Nếu gặp phải lỗi "blocked by CORS policy", người dùng cần phải thiết lập chính sách CORS phù hợp cho Bucket của mình, điều này có thể dễ dàng thực hiện trên web console bằng cách truy cập vào Bucket, mục Permission Control -> CORS:
Ali_oss_cors

4. Download

Phần download cũng có thể thực hiện tương tự, với phần frontend có thể triển khai như:

function downloadFile(filename) {
            try {
              const response = await fetch(`/download?file_name=${filename}`);
            if (!response.ok) {
               throw new Error('Failed to fetch presigned URL');
            }
            const data = await response.json();
            const downloadUrl = data.downloadUrl;
            const link = document.createElement('a');
            link.href = downloadUrl;
            link.download = filename;  
            document.body.appendChild(link); 
            link.click();  
            document.body.removeChild(link);
        } catch (error) {
            console.error('Error downloading file:', error);
        }
}

Và trên flask server:

@app.route('/download', methods=['GET'])
def dowload():
    file_name = request.args.get('file_name')
    try:
       dowload_url = s3_client.generate_presigned_url('get_object',
                                                         Params={'Bucket': BUCKET, 'Key': file_name,'ResponseContentDisposition':'attachment'},
                                                         ExpiresIn=60)
       return jsonify(downloadUrl=dowload_url)
    except NoCredentialsError:
        return jsonify({'error': 'credentials not found'}), 403

5. Tổng kết

Ở trên là những đoạn code đơn giản, minh họa một phương thức triển khai dễ dàng cho việc tải file với oss nhằm giảm tải cho webserver, tuy nhiên, giới hạn tải file dạng simple (tải 1 file trong 1 lần) chỉ là 5GB, để tải các file lớn hơn cần dùng kỹ thuật multipart-upload. Và việc dùng presign url cho phép client tương tác trực tiếp với oss mà không thông qua server, khiến việc kiểm soát trở nên khó hơn (như server sẽ không biết khi nào việc upload hoàn tất, hay file đã có trên oss thành công hay chưa). Nên OSS cũng hỗ trợ upload callbacks để quy trình hoàn thiện hơn. Ở bài viết sau, mình sẽ viết thêm về chủ đề này.

Tham khảo

1. OSS use case: Upload objects directly to OSS from clients
2. OSS Document
3.Authorize access to OSS resources by using OSS SDK for Java
4.Upload local files with signed URLs

0 0 0
Share on

Nguyen Phuc Khang

5 posts | 1 followers

You may also like

Comments

Nguyen Phuc Khang

5 posts | 1 followers

Related Products