All Products
Search
Document Center

Object Storage Service:Unggah objek menggunakan URL yang ditandatangani sebelumnya (Python SDK V2)

Last Updated:Mar 20, 2026

Secara default, objek OSS bersifat privat—hanya pemilik bucket yang dapat mengunggah ke dalamnya. Hasilkan URL yang ditandatangani sebelumnya dengan OSS SDK untuk Python 2.0 guna memberikan akses unggah sementara kepada klien HTTP mana pun tanpa membagikan kredensial Anda.

URL yang ditandatangani sebelumnya menyematkan signature V4 dan dapat digunakan beberapa kali hingga kedaluwarsa. Jika kunci objek yang sama diunggah lebih dari sekali, setiap unggahan akan menimpa objek sebelumnya. Setelah URL kedaluwarsa, Anda harus membuat URL yang ditandatangani sebelumnya baru untuk melanjutkan unggahan.

Catatan penggunaan

Cara kerja

Diagram berikut menunjukkan alur unggah menggunakan URL yang ditandatangani sebelumnya yang memungkinkan permintaan HTTP PUT.

image

Referensi metode presign()

Gunakan metode presign() untuk menghasilkan URL yang ditandatangani sebelumnya untuk PutObjectRequest:

presign(request: PutObjectRequest, **kwargs) → PresignResult

Parameter

ParameterTipeWajibDefaultDeskripsi
requestPutObjectRequestYaPermintaan unggah yang akan ditandatangani. Lihat Client.presign.
expiresdatetime.timedeltaTidak15 menitPeriode validitas relatif sejak waktu penandatanganan. Maksimum: 7 hari.
expirationdatetime.datetimeTidakWaktu kedaluwarsa absolut. Jika expiration dan expires keduanya diatur, expiration memiliki prioritas lebih tinggi.

Nilai kembalian

presign() mengembalikan objek PresignResult:

FieldTipeDeskripsi
methodstrMetode HTTP untuk permintaan unggah (PUT untuk PutObject).
urlstrURL yang ditandatangani sebelumnya untuk dibagikan kepada pengunggah.
expirationdatetimeWaktu kedaluwarsa absolut URL tersebut.
signed_headersMutableMappingHeader permintaan yang disertakan dalam tanda tangan. Pengunggah harus mengirim header ini pada setiap permintaan PUT — jika tidak, akan terjadi error SignatureNotMatch.

Untuk detail API lengkap, lihat presign.

Buat URL yang ditandatangani sebelumnya

Contoh berikut menghasilkan URL yang ditandatangani sebelumnya dengan masa berlaku 3.600 detik (1 jam).

Penting

Jika Anda menyertakan header permintaan (seperti Content-Type, kelas penyimpanan, atau metadata pengguna) saat membuat URL, header yang sama tersebut harus dikirim pada setiap permintaan PUT yang menggunakan URL tersebut. Header merupakan bagian dari tanda tangan—ketidaksesuaian akan menyebabkan error SignatureNotMatch.

import argparse
import alibabacloud_oss_v2 as oss
from datetime import timedelta

parser = argparse.ArgumentParser(description="presign put object sample")
parser.add_argument('--region', help='The region in which the bucket is located.', required=True)
parser.add_argument('--bucket', help='The name of the bucket.', required=True)
parser.add_argument('--endpoint', help='The domain names that other services can use to access OSS')
parser.add_argument('--key', help='The name of the object.', required=True)

def main():
    args = parser.parse_args()

    # Muat kredensial dari variabel lingkungan — jangan pernah menyematkan kunci secara langsung di kode sumber.
    credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()

    cfg = oss.config.load_default()
    cfg.credentials_provider = credentials_provider
    cfg.region = args.region

    if args.endpoint is not None:
        cfg.endpoint = args.endpoint

    client = oss.Client(cfg)

    # Hasilkan URL yang ditandatangani sebelumnya untuk permintaan PUT, berlaku selama 1 jam.
    pre_result = client.presign(
        oss.PutObjectRequest(
            bucket=args.bucket,
            key=args.key,
        ),
        expires=timedelta(seconds=3600)
    )

    print(f'method: {pre_result.method}')
    print(f'expiration: {pre_result.expiration.strftime("%Y-%m-%dT%H:%M:%S.000Z")}')
    print(f'url: {pre_result.url}')

    # signed_headers mencantumkan semua header yang disertakan dalam tanda tangan.
    # Pengunggah harus mengirim header ini bersama permintaan PUT.
    for key, value in pre_result.signed_headers.items():
        print(f'signed header: {key}: {value}')

if __name__ == "__main__":
    main()

Untuk contoh lengkap, lihat presigner_put_object.py.

Unggah objek menggunakan URL yang ditandatangani sebelumnya

Berikan URL yang ditandatangani sebelumnya ke klien HTTP mana pun untuk mengunggah. Semua contoh di bawah menggunakan HTTP PUT biasa—tidak diperlukan OSS SDK di sisi klien.

curl

curl -X PUT -T /path/to/local/file "https://exampleobject.oss-cn-hangzhou.aliyuncs.com/exampleobject.txt?x-oss-date=20241112T083238Z&x-oss-expires=3599&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI****************%2F20241112%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-signature=ed5a******************************************************"

Python

import requests

def upload_file(signed_url, file_path):
    try:
        with open(file_path, 'rb') as file:
            response = requests.put(signed_url, data=file)

        print(f"Status code: {response.status_code}")
        if response.status_code == 200:
            print("Upload successful.")
        print(response.text)

    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    # Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya yang dihasilkan di atas.
    signed_url = "<signedUrl>"
    file_path = "C:\\Users\\demo.txt"
    upload_file(signed_url, file_path)

Java

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;
import java.util.*;

public class SignUrlUpload {
    public static void main(String[] args) throws Throwable {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;

        URL signedUrl = new URL("<signedUrl>");
        String pathName = "C:\\Users\\demo.txt";

        Map<String, String> headers = new HashMap<>();
        headers.put("x-oss-callback", "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9");
        headers.put("x-oss-callback-var", "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==");

        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);

            for (Map.Entry header : headers.entrySet()) {
                put.addHeader(header.getKey().toString(), header.getValue().toString());
            }

            httpClient = HttpClients.createDefault();
            response = httpClient.execute(put);

            System.out.println("Kode status: " + response.getStatusLine().getStatusCode());
            if (response.getStatusLine().getStatusCode() == 200) {
                System.out.println("Unggahan berhasil.");
            }
            System.out.println(response.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
            httpClient.close();
        }
    }
}

Go

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func uploadFile(signedUrl, filePath string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return fmt.Errorf("unable to open file: %w", err)
    }
    defer file.Close()

    client := &http.Client{}
    req, err := http.NewRequest("PUT", signedUrl, file)
    if err != nil {
        return fmt.Errorf("failed to create request: %w", err)
    }

    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("failed to send request: %w", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return fmt.Errorf("failed to read response: %w", err)
    }

    fmt.Printf("Status code: %d\n", resp.StatusCode)
    if resp.StatusCode == 200 {
        fmt.Println("Upload successful.")
    }
    fmt.Println(string(body))
    return nil
}

func main() {
    // Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya.
    signedUrl := "<signedUrl>"
    filePath := "C:\\Users\\demo.txt"

    if err := uploadFile(signedUrl, filePath); err != nil {
        fmt.Println("An error occurred:", err)
    }
}

Node.js

const fs = require('fs');
const axios = require('axios');

async function uploadFile(signedUrl, filePath) {
    try {
        const fileStream = fs.createReadStream(filePath);
        const response = await axios.put(signedUrl, fileStream, {
            headers: {
                'Content-Type': 'application/octet-stream'
            }
        });

        console.log(`Status code: ${response.status}`);
        if (response.status === 200) {
            console.log("Upload successful.");
        }
        console.log(response.data);
    } catch (error) {
        console.error(`An error occurred: ${error.message}`);
    }
}

(async () => {
    // Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya.
    const signedUrl = '<signedUrl>';
    const filePath = 'C:\\Users\\demo.txt';
    await uploadFile(signedUrl, filePath);
})();

Browser.js

Penting

Browser secara otomatis menambahkan header Content-Type ke permintaan PUT. Jika Content-Type tidak disertakan saat membuat URL yang ditandatangani sebelumnya, tanda tangan tidak akan sesuai dan OSS mengembalikan 403 SignatureNotMatch. Selalu tentukan Content-Type saat membuat URL yang ditandatangani sebelumnya untuk penggunaan browser.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload Example</title>
</head>
<body>
    <h1>File Upload Example</h1>
    <input type="file" id="fileInput" />
    <button id="uploadButton">Upload File</button>

    <script>
        // Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya yang dihasilkan pada langkah 1.
        const signedUrl = "<signedUrl>";

        document.getElementById('uploadButton').addEventListener('click', async () => {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];

            if (!file) {
                alert('Please select a file to upload.');
                return;
            }

            try {
                await upload(file, signedUrl);
                alert('File uploaded successfully!');
            } catch (error) {
                console.error('Error during upload:', error);
                alert('Upload failed: ' + error.message);
            }
        });

        const upload = async (file, presignedUrl) => {
            const response = await fetch(presignedUrl, {
                method: 'PUT',
                body: file,
            });

            if (!response.ok) {
                throw new Error(`Upload failed, status: ${response.status}`);
            }

            console.log('File uploaded successfully');
        };
    </script>
</body>
</html>

C#

using System.Net.Http.Headers;

// Tentukan path lengkap file lokal yang akan diunggah.
var filePath = "C:\\Users\\demo.txt";
// Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya.
var presignedUrl = "<signedUrl>";

using var httpClient = new HttpClient();
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var content = new StreamContent(fileStream);

var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl);
request.Content = content;

var response = await httpClient.SendAsync(request);

if (response.IsSuccessStatusCode)
{
    Console.WriteLine($"Uploaded! Status code: {response.StatusCode}");
    Console.WriteLine("Response headers:");
    foreach (var header in response.Headers)
    {
        Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
    }
}
else
{
    string responseContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"Upload failed! Status code: {response.StatusCode}");
    Console.WriteLine("Response content: " + responseContent);
}

C++

#include <iostream>
#include <fstream>
#include <curl/curl.h>

void uploadFile(const std::string& signedUrl, const std::string& filePath) {
    CURL *curl;
    CURLcode res;

    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();

    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, signedUrl.c_str());
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);

        FILE *file = fopen(filePath.c_str(), "rb");
        if (!file) {
            std::cerr << "Unable to open file: " << filePath << std::endl;
            return;
        }

        fseek(file, 0, SEEK_END);
        long fileSize = ftell(file);
        fseek(file, 0, SEEK_SET);

        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize);
        curl_easy_setopt(curl, CURLOPT_READDATA, file);

        res = curl_easy_perform(curl);

        if (res != CURLE_OK) {
            std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
        } else {
            long httpCode = 0;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
            std::cout << "Status code: " << httpCode << std::endl;
            if (httpCode == 200) {
                std::cout << "Upload successful." << std::endl;
            }
        }

        fclose(file);
        curl_easy_cleanup(curl);
    }

    curl_global_cleanup();
}

int main() {
    // Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya.
    std::string signedUrl = "<signedUrl>";
    std::string filePath = "C:\\Users\\demo.txt";
    uploadFile(signedUrl, filePath);
    return 0;
}

Android

package com.example.signurlupload;

import android.os.AsyncTask;
import android.util.Log;

import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

public class SignUrlUploadActivity {

    private static final String TAG = "SignUrlUploadActivity";

    public void uploadFile(String signedUrl, String filePath) {
        new UploadTask().execute(signedUrl, filePath);
    }

    private class UploadTask extends AsyncTask<String, Void, String> {

        @Override
        protected String doInBackground(String... params) {
            String signedUrl = params[0];
            String filePath = params[1];

            HttpURLConnection connection = null;
            DataOutputStream dos = null;
            FileInputStream fis = null;

            try {
                URL url = new URL(signedUrl);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("PUT");
                connection.setDoOutput(true);
                connection.setRequestProperty("Content-Type", "application/octet-stream");

                fis = new FileInputStream(filePath);
                dos = new DataOutputStream(connection.getOutputStream());

                byte[] buffer = new byte[1024];
                int length;
                while ((length = fis.read(buffer)) != -1) {
                    dos.write(buffer, 0, length);
                }

                dos.flush();
                dos.close();
                fis.close();

                int responseCode = connection.getResponseCode();
                Log.d(TAG, "Status code: " + responseCode);
                if (responseCode == 200) {
                    Log.d(TAG, "Upload successful.");
                }

                return "Upload complete. Status code: " + responseCode;

            } catch (IOException e) {
                e.printStackTrace();
                return "Upload failed: " + e.getMessage();
            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }
        }

        @Override
        protected void onPostExecute(String result) {
            Log.d(TAG, result);
        }
    }

    public static void main(String[] args) {
        SignUrlUploadActivity activity = new SignUrlUploadActivity();
        // Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya.
        String signedUrl = "<signedUrl>";
        String filePath = "C:\\Users\\demo.txt";
        activity.uploadFile(signedUrl, filePath);
    }
}

Skenario umum

Unggah dengan header permintaan dan metadata pengguna

Sertakan Content-Type, kelas penyimpanan, atau metadata pengguna saat membuat URL. Pengunggah harus mengirim header yang sama—header tersebut merupakan bagian dari tanda tangan.

Langkah 1: Buat URL

import argparse
import alibabacloud_oss_v2 as oss
from datetime import timedelta

parser = argparse.ArgumentParser(description="presign put object sample")
parser.add_argument('--region', required=True)
parser.add_argument('--bucket', required=True)
parser.add_argument('--endpoint')
parser.add_argument('--key', required=True)

def main():
    args = parser.parse_args()

    credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
    cfg = oss.config.load_default()
    cfg.credentials_provider = credentials_provider
    cfg.region = args.region

    if args.endpoint is not None:
        cfg.endpoint = args.endpoint

    client = oss.Client(cfg)

    pre_result = client.presign(
        oss.PutObjectRequest(
            bucket=args.bucket,
            key=args.key,
            content_type='text/plain;charset=utf8',  # Ditandatangani ke dalam URL — pengunggah harus sesuai dengan ini.
            storage_class='Standard',                 # Ditandatangani ke dalam URL — pengunggah harus sesuai dengan ini.
            metadata={
                'key1': 'value1',  # Ditandatangani sebagai x-oss-meta-key1 — pengunggah harus sesuai dengan ini.
                'key2': 'value2',
            }
        ),
        expires=timedelta(seconds=3600)
    )

    print(f'method: {pre_result.method}')
    print(f'expiration: {pre_result.expiration.strftime("%Y-%m-%dT%H:%M:%S.000Z")}')
    print(f'url: {pre_result.url}')

    for key, value in pre_result.signed_headers.items():
        print(f'signed header: {key}: {value}')

if __name__ == "__main__":
    main()

Langkah 2: Unggah dengan header yang sesuai

Semua klien yang mengirim permintaan PUT harus menyertakan header yang sama seperti yang digunakan saat membuat URL.

curl

curl -X PUT \
     -H "Content-Type: text/plain;charset=utf8" \
     -H "x-oss-storage-class: Standard" \
     -H "x-oss-meta-key1: value1" \
     -H "x-oss-meta-key2: value2" \
     -T "C:\\Users\\demo.txt" \
     "https://exampleobject.oss-cn-hangzhou.aliyuncs.com/exampleobject.txt?x-oss-date=20241112T083238Z&x-oss-expires=3599&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI****************%2F20241112%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-signature=ed5a******************************************************"

Python

import requests

def upload_file(signed_url, file_path, headers=None, metadata=None):
    """
    :param signed_url: The presigned URL.
    :param file_path: Full path of the local file to upload.
    :param headers: Request headers (must match those used when generating the URL).
    :param metadata: User metadata (must match; the SDK adds the x-oss-meta- prefix automatically).
    """
    if not headers:
        headers = {}
    if not metadata:
        metadata = {}

    # SDK menambahkan awalan x-oss-meta- saat menandatangani — tiru hal ini di sini.
    for key, value in metadata.items():
        headers[f'x-oss-meta-{key}'] = value

    try:
        with open(file_path, 'rb') as file:
            response = requests.put(signed_url, data=file, headers=headers)
            print(f"Status code: {response.status_code}")
            if response.status_code == 200:
                print("Upload successful.")
            else:
                print("Upload failed.")
            print(response.text)
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    signed_url = "<signedUrl>"
    file_path = "C:\\Users\\demo.txt"

    headers = {
        "Content-Type": "text/plain;charset=utf8",
        "x-oss-storage-class": "Standard"
    }
    metadata = {
        "key1": "value1",
        "key2": "value2"
    }

    upload_file(signed_url, file_path, headers, metadata)

Java

import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.model.StorageClass;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;
import java.util.*;

public class SignUrlUpload {
    public static void main(String[] args) throws Throwable {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;

        URL signedUrl = new URL("<signedUrl>");
        String pathName = "C:\\Users\\demo.txt";

        // Header ini harus persis sesuai dengan yang ditentukan saat membuat URL.
        Map<String, String> headers = new HashMap<>();
        headers.put(OSSHeaders.STORAGE_CLASS, StorageClass.Standard.toString());
        headers.put(OSSHeaders.CONTENT_TYPE, "text/plain;charset=utf8");

        // Metadata pengguna juga harus sesuai; SDK menambahkan awalan x-oss-meta-.
        Map<String, String> userMetadata = new HashMap<>();
        userMetadata.put("key1", "value1");
        userMetadata.put("key2", "value2");

        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);

            for (Map.Entry header : headers.entrySet()) {
                put.addHeader(header.getKey().toString(), header.getValue().toString());
            }
            for (Map.Entry meta : userMetadata.entrySet()) {
                put.addHeader("x-oss-meta-" + meta.getKey().toString(), meta.getValue().toString());
            }

            httpClient = HttpClients.createDefault();
            response = httpClient.execute(put);

            System.out.println("Status code: " + response.getStatusLine().getStatusCode());
            if (response.getStatusLine().getStatusCode() == 200) {
                System.out.println("Upload successful.");
            }
            System.out.println(response.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
            httpClient.close();
        }
    }
}

Go

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
)

func uploadFile(signedUrl string, filePath string, headers map[string]string, metadata map[string]string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    fileBytes, err := ioutil.ReadAll(file)
    if err != nil {
        return err
    }

    req, err := http.NewRequest("PUT", signedUrl, bytes.NewBuffer(fileBytes))
    if err != nil {
        return err
    }

    // Ini harus sesuai dengan header yang ditentukan saat membuat URL.
    for key, value := range headers {
        req.Header.Set(key, value)
    }
    for key, value := range metadata {
        req.Header.Set(fmt.Sprintf("x-oss-meta-%s", key), value)
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    fmt.Printf("Status code: %d\n", resp.StatusCode)
    if resp.StatusCode == 200 {
        fmt.Println("Upload successful.")
    } else {
        fmt.Println("Upload failed.")
    }
    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
    return nil
}

func main() {
    signedUrl := "<signedUrl>"
    filePath := "C:\\Users\\demo.txt"

    headers := map[string]string{
        "Content-Type":        "text/plain;charset=utf8",
        "x-oss-storage-class": "Standard",
    }
    metadata := map[string]string{
        "key1": "value1",
        "key2": "value2",
    }

    if err := uploadFile(signedUrl, filePath, headers, metadata); err != nil {
        fmt.Printf("An error occurred: %v\n", err)
    }
}

Node.js

const fs = require('fs');
const axios = require('axios');

async function uploadFile(signedUrl, filePath, headers = {}, metadata = {}) {
    try {
        // SDK menambahkan awalan x-oss-meta- saat menandatangani — tiru hal ini di sini.
        for (const [key, value] of Object.entries(metadata)) {
            headers[`x-oss-meta-${key}`] = value;
        }

        const fileStream = fs.createReadStream(filePath);
        const response = await axios.put(signedUrl, fileStream, { headers });

        console.log(`Status code: ${response.status}`);
        if (response.status === 200) {
            console.log("Upload successful.");
        } else {
            console.log("Upload failed.");
        }
        console.log(response.data);
    } catch (error) {
        console.error(`An error occurred: ${error.message}`);
    }
}

(async () => {
    const signedUrl = "<signedUrl>";
    const filePath = "C:\\Users\\demo.txt";
    const headers = {
        "Content-Type": "text/plain;charset=utf8",
        "x-oss-storage-class": "Standard"
    };
    const metadata = {
        "key1": "value1",
        "key2": "value2"
    };
    await uploadFile(signedUrl, filePath, headers, metadata);
})();

Browser.js

Penting

Browser secara otomatis menambahkan header Content-Type. Tentukan Content-Type saat membuat URL untuk mencegah error 403 SignatureNotMatch.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload Example</title>
</head>
<body>
    <h1>File Upload Example</h1>
    <input type="file" id="fileInput" />
    <button id="uploadButton">Upload File</button>

    <script>
        const signedUrl = "<signedUrl>";

        document.getElementById('uploadButton').addEventListener('click', async () => {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];

            if (file) {
                try {
                    await upload(file, signedUrl);
                } catch (error) {
                    console.error('Error during upload:', error);
                    alert('Upload failed: ' + error.message);
                }
            } else {
                alert('Please select a file to upload.');
            }
        });

        const upload = async (file, presignedUrl) => {
            // Header ini harus persis sesuai dengan yang ditentukan saat membuat URL.
            const headers = {
                "Content-Type": "text/plain;charset=utf8",
                'x-oss-storage-class': 'Standard',
                'x-oss-meta-key1': 'value1',
                'x-oss-meta-key2': 'value2'
            };

            const response = await fetch(presignedUrl, {
                method: 'PUT',
                headers: headers,
                body: file
            });

            if (!response.ok) {
                throw new Error(`Upload failed, status: ${response.status}`);
            }

            alert('File uploaded successfully');
            console.log('File uploaded successfully');
        };
    </script>
</body>
</html>

C#

using System.Net.Http.Headers;

var filePath = "C:\\Users\\demo.txt";
var presignedUrl = "<signedUrl>";

using var httpClient = new HttpClient();
using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using var content = new StreamContent(fileStream);

var request = new HttpRequestMessage(HttpMethod.Put, presignedUrl);
request.Content = content;

// Header ini harus persis sesuai dengan yang ditentukan saat membuat URL.
request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain") { CharSet = "utf8" };
request.Content.Headers.Add("x-oss-meta-key1", "value1");
request.Content.Headers.Add("x-oss-meta-key2", "value2");
request.Content.Headers.Add("x-oss-storage-class", "Standard");

Console.WriteLine("Request headers:");
foreach (var header in request.Content.Headers)
{
    Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
}

var response = await httpClient.SendAsync(request);

if (response.IsSuccessStatusCode)
{
    Console.WriteLine($"Uploaded! Status code: {response.StatusCode}");
    Console.WriteLine("Response headers:");
    foreach (var header in response.Headers)
    {
        Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
    }
}
else
{
    string responseContent = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"Upload failed! Status code: {response.StatusCode}");
    Console.WriteLine("Response content: " + responseContent);
}

C++

#include <iostream>
#include <fstream>
#include <curl/curl.h>
#include <map>
#include <string>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) {
    size_t totalSize = size * nmemb;
    output->append((char*)contents, totalSize);
    return totalSize;
}

void uploadFile(const std::string& signedUrl, const std::string& filePath,
                const std::map<std::string, std::string>& headers,
                const std::map<std::string, std::string>& metadata) {
    CURL* curl;
    CURLcode res;
    std::string readBuffer;

    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();

    if (curl) {
        curl_easy_setopt(curl, CURLOPT_URL, signedUrl.c_str());
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);

        FILE* file = fopen(filePath.c_str(), "rb");
        if (!file) {
            std::cerr << "Unable to open file: " << filePath << std::endl;
            return;
        }

        fseek(file, 0, SEEK_END);
        long fileSize = ftell(file);
        rewind(file);

        curl_easy_setopt(curl, CURLOPT_READDATA, file);
        curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize);

        // Header ini harus persis sesuai dengan yang ditentukan saat membuat URL.
        struct curl_slist* chunk = nullptr;
        for (const auto& header : headers) {
            std::string headerStr = header.first + ": " + header.second;
            chunk = curl_slist_append(chunk, headerStr.c_str());
        }
        for (const auto& meta : metadata) {
            std::string metaStr = "x-oss-meta-" + meta.first + ": " + meta.second;
            chunk = curl_slist_append(chunk, metaStr.c_str());
        }
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);

        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer);

        res = curl_easy_perform(curl);

        if (res != CURLE_OK) {
            std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
        } else {
            long responseCode;
            curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &responseCode);
            std::cout << "Status code: " << responseCode << std::endl;
            if (responseCode == 200) {
                std::cout << "Upload successful." << std::endl;
            } else {
                std::cout << "Upload failed." << std::endl;
            }
            std::cout << readBuffer << std::endl;
        }

        fclose(file);
        curl_slist_free_all(chunk);
        curl_easy_cleanup(curl);
    }

    curl_global_cleanup();
}

int main() {
    std::string signedUrl = "<signedUrl>";
    std::string filePath = "C:\\Users\\demo.txt";

    std::map<std::string, std::string> headers = {
        {"Content-Type", "text/plain;charset=utf8"},
        {"x-oss-storage-class", "Standard"}
    };
    std::map<std::string, std::string> metadata = {
        {"key1", "value1"},
        {"key2", "value2"}
    };

    uploadFile(signedUrl, filePath, headers, metadata);
    return 0;
}

Android

import android.os.AsyncTask;
import android.util.Log;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

public class SignUrlUploadActivity extends AppCompatActivity {

    private static final String TAG = "SignUrlUploadActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String signedUrl = "<signedUrl>";
        String pathName = "/storage/emulated/0/demo.txt";

        // Header ini harus persis sesuai dengan yang ditentukan saat membuat URL.
        Map<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "text/plain;charset=utf8");
        headers.put("x-oss-storage-class", "Standard");

        Map<String, String> userMetadata = new HashMap<>();
        userMetadata.put("key1", "value1");
        userMetadata.put("key2", "value2");

        new UploadTask().execute(signedUrl, pathName, headers, userMetadata);
    }

    private class UploadTask extends AsyncTask<Object, Void, Integer> {
        @Override
        protected Integer doInBackground(Object... params) {
            String signedUrl = (String) params[0];
            String pathName = (String) params[1];
            Map<String, String> headers = (Map<String, String>) params[2];
            Map<String, String> userMetadata = (Map<String, String>) params[3];

            try {
                URL url = new URL(signedUrl);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("PUT");
                connection.setDoOutput(true);
                connection.setUseCaches(false);

                for (Entry<String, String> header : headers.entrySet()) {
                    connection.setRequestProperty(header.getKey(), header.getValue());
                }
                for (Entry<String, String> meta : userMetadata.entrySet()) {
                    connection.setRequestProperty("x-oss-meta-" + meta.getKey(), meta.getValue());
                }

                File file = new File(pathName);
                FileInputStream fileInputStream = new FileInputStream(file);
                DataOutputStream dos = new DataOutputStream(connection.getOutputStream());

                byte[] buffer = new byte[1024];
                int count;
                while ((count = fileInputStream.read(buffer)) != -1) {
                    dos.write(buffer, 0, count);
                }

                fileInputStream.close();
                dos.flush();
                dos.close();

                int responseCode = connection.getResponseCode();
                Log.d(TAG, "Status code: " + responseCode);
                if (responseCode == 200) {
                    Log.d(TAG, "Upload successful.");
                } else {
                    Log.d(TAG, "Upload failed.");
                }

                InputStream is = connection.getInputStream();
                byte[] responseBuffer = new byte[1024];
                StringBuilder responseStringBuilder = new StringBuilder();
                while ((count = is.read(responseBuffer)) != -1) {
                    responseStringBuilder.append(new String(responseBuffer, 0, count));
                }
                Log.d(TAG, responseStringBuilder.toString());

                return responseCode;
            } catch (IOException e) {
                e.printStackTrace();
                return -1;
            }
        }

        @Override
        protected void onPostExecute(Integer result) {
            super.onPostExecute(result);
            if (result == 200) {
                Toast.makeText(SignUrlUploadActivity.this, "Upload successful", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(SignUrlUploadActivity.this, "Upload failed", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

Unggah menggunakan unggah multi-bagian

Untuk file besar, gunakan URL yang ditandatangani sebelumnya dengan unggah multi-bagian. Hasilkan URL yang ditandatangani sebelumnya terpisah untuk setiap bagian.

Untuk contoh lengkap, lihat presigner_complete_multipart_upload.py.

import argparse
import os
import requests
import alibabacloud_oss_v2 as oss

parser = argparse.ArgumentParser(description="presign multipart upload sample")
parser.add_argument('--region', required=True)
parser.add_argument('--bucket', required=True)
parser.add_argument('--endpoint')
parser.add_argument('--key', required=True)
parser.add_argument('--file_path', required=True)

def main():
    args = parser.parse_args()

    credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
    cfg = oss.config.load_default()
    cfg.credentials_provider = credentials_provider
    cfg.region = args.region

    if args.endpoint is not None:
        cfg.endpoint = args.endpoint

    client = oss.Client(cfg)

    part_size = 1024 * 1024  # 1 MB per bagian
    data_size = os.path.getsize(args.file_path)
    part_number = 1
    upload_parts = []

    # Langkah 1: Mulai unggah multi-bagian dan dapatkan ID unggahan.
    init_pre_result = client.presign(oss.InitiateMultipartUploadRequest(
        bucket=args.bucket,
        key=args.key,
    ))

    with requests.post(init_pre_result.url, headers=init_pre_result.signed_headers) as resp:
        obj = oss.InitiateMultipartUploadResult()
        oss.serde.deserialize_xml(xml_data=resp.content, obj=obj)

        # Langkah 2: Unggah setiap bagian menggunakan URL yang ditandatangani sebelumnya masing-masing.
        with open(args.file_path, 'rb') as f:
            for start in range(0, data_size, part_size):
                n = part_size
                if start + n > data_size:
                    n = data_size - start
                reader = oss.io_utils.SectionReader(oss.io_utils.ReadAtReader(f), start, n)

                up_pre_result = client.presign(oss.UploadPartRequest(
                    bucket=args.bucket,
                    key=args.key,
                    upload_id=obj.upload_id,
                    part_number=part_number,
                ))

                with requests.put(up_pre_result.url, headers=up_pre_result.signed_headers, data=reader) as up_result:
                    print(f'status code: {up_result.status_code},'
                          f' part number: {part_number},'
                          f' etag: {up_result.headers.get("ETag")}')

                    upload_parts.append(oss.UploadPart(
                        part_number=part_number,
                        etag=up_result.headers.get("ETag")
                    ))
                part_number += 1

        # Langkah 3: Selesaikan unggah multi-bagian.
        parts = sorted(upload_parts, key=lambda p: p.part_number)
        request = oss.CompleteMultipartUploadRequest(
            bucket=args.bucket,
            key=args.key,
            upload_id=obj.upload_id,
            complete_multipart_upload=oss.CompleteMultipartUpload(parts=parts)
        )

        op_input = oss.serde.serialize_input(request, oss.OperationInput(
            op_name='CompleteMultipartUpload',
            method='POST',
            bucket=request.bucket,
        ))

        complete_pre_result = client.presign(request)

        with requests.post(complete_pre_result.url, headers=complete_pre_result.signed_headers, data=op_input.body) as complete_resp:
            result = oss.CompleteMultipartUploadResult()
            oss.serde.deserialize_xml(xml_data=complete_resp.content, obj=result)
            print(f'status code: {complete_resp.status_code},'
                  f' etag: {complete_resp.headers.get("ETag")},'
                  f' url: {result.location}')

if __name__ == "__main__":
    main()

Unggah dengan callback unggahan

Gunakan callback unggahan agar OSS memberi tahu server Anda saat unggahan selesai.

Langkah 1: Buat URL yang ditandatangani sebelumnya dengan parameter callback

import argparse
import base64
import alibabacloud_oss_v2 as oss
from datetime import timedelta

parser = argparse.ArgumentParser(description="presign put object sample")
parser.add_argument('--region', required=True)
parser.add_argument('--bucket', required=True)
parser.add_argument('--endpoint')
parser.add_argument('--key', required=True)

def main():
    args = parser.parse_args()

    credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
    cfg = oss.config.load_default()
    cfg.credentials_provider = credentials_provider
    cfg.region = args.region

    if args.endpoint is not None:
        cfg.endpoint = args.endpoint

    client = oss.Client(cfg)

    call_back_url = "http://www.example.com/callback"
    # Encode konfigurasi callback dalam Base64.
    callback = base64.b64encode(
        str('{"callbackUrl":"' + call_back_url + '","callbackBody":"bucket=${bucket}&object=${object}&my_var_1=${x:var1}&my_var_2=${x:var2}"}').encode()
    ).decode()
    callback_var = base64.b64encode(
        '{"x:var1":"value1","x:var2":"value2"}'.encode()
    ).decode()

    pre_result = client.presign(
        oss.PutObjectRequest(
            bucket=args.bucket,
            key=args.key,
            callback=callback,
            callback_var=callback_var,
        ),
        expires=timedelta(seconds=3600)
    )

    print(f'method: {pre_result.method}')
    print(f'expiration: {pre_result.expiration.strftime("%Y-%m-%dT%H:%M:%S.000Z")}')
    print(f'url: {pre_result.url}')

    for key, value in pre_result.signed_headers.items():
        print(f'signed header: {key}: {value}')

if __name__ == "__main__":
    main()

Langkah 2: Unggah menggunakan URL yang ditandatangani sebelumnya

curl

curl -X PUT \
     -H "x-oss-callback: eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9" \
     -H "x-oss-callback-var: eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==" \
     -T "C:\\Users\\demo.txt" \
     "https://exampleobject.oss-cn-hangzhou.aliyuncs.com/exampleobject.txt?x-oss-date=20241112T083238Z&x-oss-expires=3599&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI****************%2F20241112%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-signature=ed5a******************************************************"

Python

import requests

def upload_file(signed_url, file_path, headers=None):
    if not headers:
        headers = {}

    try:
        with open(file_path, 'rb') as file:
            response = requests.put(signed_url, data=file, headers=headers)
            print(f"Status code: {response.status_code}")
            if response.status_code == 200:
                print("Upload successful.")
            else:
                print("Upload failed.")
            print(response.text)
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    signed_url = "<signedUrl>"
    file_path = "C:\\Users\\demo.txt"

    headers = {
        "x-oss-callback": "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9",
        "x-oss-callback-var": "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==",
    }

    upload_file(signed_url, file_path, headers)

Go

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
)

func uploadFile(signedUrl string, filePath string, headers map[string]string) error {
    file, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer file.Close()

    fileBytes, err := io.ReadAll(file)
    if err != nil {
        return err
    }

    req, err := http.NewRequest("PUT", signedUrl, bytes.NewBuffer(fileBytes))
    if err != nil {
        return err
    }

    for key, value := range headers {
        req.Header.Add(key, value)
    }

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    fmt.Printf("Status code: %d\n", resp.StatusCode)
    if resp.StatusCode == 200 {
        fmt.Println("Upload successful.")
    } else {
        fmt.Println("Upload failed.")
    }
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
    return nil
}

func main() {
    signedUrl := "<signedUrl>"
    filePath := "C:\\Users\\demo.txt"

    headers := map[string]string{
        "x-oss-callback":     "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9",
        "x-oss-callback-var": "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==",
    }

    if err := uploadFile(signedUrl, filePath, headers); err != nil {
        fmt.Printf("An error occurred: %v\n", err)
    }
}

Java

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.FileEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import java.io.*;
import java.net.URL;
import java.util.*;

public class SignUrlUpload {
    public static void main(String[] args) throws Throwable {
        CloseableHttpClient httpClient = null;
        CloseableHttpResponse response = null;

        URL signedUrl = new URL("<signedUrl>");
        String pathName = "C:\\Users\\demo.txt";

        Map<String, String> headers = new HashMap<>();
        headers.put("x-oss-callback", "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9");
        headers.put("x-oss-callback-var", "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==");

        try {
            HttpPut put = new HttpPut(signedUrl.toString());
            HttpEntity entity = new FileEntity(new File(pathName));
            put.setEntity(entity);

            for (Map.Entry header : headers.entrySet()) {
                put.addHeader(header.getKey().toString(), header.getValue().toString());
            }

            httpClient = HttpClients.createDefault();
            response = httpClient.execute(put);

            System.out.println("Status code: " + response.getStatusLine().getStatusCode());
            if (response.getStatusLine().getStatusCode() == 200) {
                System.out.println("Upload successful.");
            }
            System.out.println(response.toString());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            response.close();
            httpClient.close();
        }
    }
}

PHP

<?php

function uploadFile($signedUrl, $filePath, $headers = []) {
    if (!file_exists($filePath)) {
        echo "The file does not exist: $filePath\n";
        return;
    }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $signedUrl);
    curl_setopt($ch, CURLOPT_PUT, true);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_INFILE, fopen($filePath, 'rb'));
    curl_setopt($ch, CURLOPT_INFILESIZE, filesize($filePath));
    curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(function($key, $value) {
        return "$key: $value";
    }, array_keys($headers), $headers));

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    echo "Status code: $httpCode\n";
    if ($httpCode == 200) {
        echo "Upload successful.\n";
    } else {
        echo "Upload failed.\n";
    }
    echo $response . "\n";
}

// Ganti <signedUrl> dengan URL yang ditandatangani sebelumnya.
$signedUrl = "<signedUrl>";
$filePath = "C:\\Users\\demo.txt";

$headers = [
    "x-oss-callback" => "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly93d3cuZXhhbXBsZS5jb20vY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJidWNrZXQ9JHtidWNrZXR9Jm9iamVjdD0ke29iamVjdH0mbXlfdmFyXzE9JHt4OnZhcjF9Jm15X3Zhcl8yPSR7eDp2YXIyfSJ9",
    "x-oss-callback-var" => "eyJ4OnZhcjEiOiJ2YWx1ZTEiLCJ4OnZhcjIiOiJ2YWx1ZTIifQ==",
];

uploadFile($signedUrl, $filePath, $headers);
?>

FAQ

Apakah URL yang ditandatangani sebelumnya yang kedaluwarsa di tengah unggahan membatalkan unggahan tersebut?

Ya. Jika unggahan dimulai sebelum URL kedaluwarsa, proses tersebut akan berjalan hingga selesai meskipun URL kedaluwarsa selama transfer. Periode validitas URL merupakan nilai minimum antara periode validitas token dan periode validitas pra-tandatangan.

Apakah saya perlu mengirim header permintaan jika saya tidak menentukan apa pun saat membuat URL?

Tidak. Header permintaan dan metadata pengguna bersifat opsional. Jika tidak ada yang ditentukan saat membuat URL, abaikan kode terkait header dari permintaan unggahan.

Langkah selanjutnya