Привіт, я Тейлор Х'юз. Я є інженером-програмістом. Я працював над додатками і вибудовував команди в компаніях Facebook, Google, Clubhouse та багатьох стартапах між ними.
Проблема з надання користувачам можливості завантажувати файли в корзину S3 виникає в кожному проєкті. Однак, визначити правильні JavaScript компоненти і налаштування, щоб зробити все це працюючим разом, видається справжнім відкриттям.
Документація AWS порадила б вам налаштувати додатковий сервіс аутентифікації та імпортувати весь SDK AWS JS у ваш клієнтський код — але цього насправді не потрібно!
Замість цього, ми можемо використовувати попередньо підписані URL та сучасні веб-API, щоб легко завантажувати файли безпосередньо в S3 з браузера із лише сотні рядків коду.
Загальне рішення, з погляду браузера користувача, виглядає так:
- Користувач натискає "Завантажити файл" і вибирає файл
- З урахуванням метаданих цього файлу, запитайте попередньо підписаний URL S3 PutObject з вашого серверного API
- З урахуванням цього попередньо підписаного URL S3, браузер виконує PUT-запит для файла за допомогою
XmlHttpRequest
та може відстежувати прогрес - Після завантаження, браузер повертає новий ключ назад до API знову для того, щоб викликати те, що ваше API робить із завантаженими файлами
- Прибуток!
Якщо ви хочете перейти до готового коду, ось gist із TypeScript фронтендом та обробником API Python як приклади.
Я піду крок за кроком у цьому пості:
Налаштування AWS
Спочатку створіть нову корзину yourproject-upload
без включеного загального доступу та без політики доступу. (Я також додав правило життєвого циклу для цієї корзини, щоб всі файли всередині видалялися після 24 годин — я переміщую завантажені файли в інше місце для загального використання.)
По-друге, додайте нового користувача IAM під назвою web-upload-only
. Отримайте доступ до ключа та секрета нового користувача та додайте ці облікові дані до вашого внутрішнього веб-сервера, якщо це зазвичай робиться.
(Ці дані для доступу повинні бути розділені від ваших основних облікових даних AWS.)
По-третє, дайте доступ s3:PutObject
до web-upload-only
для будь-яких шляхів у вашому новому кошику yourproject-upload/*
. (Ми обмежимо ключі, які можна записувати, коли ми повернемо підписані URL-адреси PutObject.)
Встроєна політика для користувача IAM web-upload-only, яка надає доступ PutObject до кошику S3.
Нарешті, також дайте доступ s3:GetObject
до кошика yourproject-upload/*
вашій звичайній ролі або користувачеві AWS. Вам знадобиться інший користувач з більшими дозволами, щоб перемістити файл з вашого кошика завантаження в основний/публічний кошик для обслуговування.
Веб-сервер: Додайте API-точку "створення завантаження"
З урахуванням доступного ключа та секретного ключа нового користувача IAM ви тепер можете створювати підписані URL-адреси, які дозволять кінцевому користувачеві записувати конкретні ключі в S3.
Вхідними даними для нової кінцевої точки будуть:
content_type
— MIME-тип файлу, який буде встановлено як Content-Type запиту PUT у браузері і повинен бути включений у підпис.filename
— початкове ім'я файлу, з якого ми можемо взяти розширення, щоб ключ S3 мав гарне розширення.
З урахуванням цих вхідних даних створіть клієнт AWS S3 та згенеруйте підписаний URL-адрес PutObject. З boto3 у Python це виглядає наступним чином:
def uploads3client() -> S3Client:
return boto3.client(
"s3",
awsaccesskeyid=settings.UPLOADAWSACCESSKEYID,
awssecretaccesskey=settings.UPLOADAWSSECRETACCESSKEY,
regionname=AWSREGION,
)
@apiview("/upload/create")
def createupload(request: Request) -> Response:
ext = request.validateddata["originalfilename"].split(".")[-1].lower()
# Включити ідентифікатор користувача та дату у створений шлях S3:
date = datetime.now().strftime("%Y%m%d")
key = f"uploads/{request.user.id}/{date}-{uuid.uuid4()}.
{ext}"
## Генерація підписаного URL:
підписаний_url_завантаження = upload_s3_client().generate_presignedurl(
"put_object",
Params={
"Bucket": "yourproject-upload",
"Key": key,
"ContentType": request.validated_data["content\type"],
},
ExpiresIn=60 * 60,
)
# Повернення ключа та підписаного URL для PutObject клієнту:
return success_response(
{"key": key, "presigned_upload_url": підписаний_url_завантаження}
)
На стороні клієнта: Пов'язання всього разом
Тепер на клієнті вам потрібно додати вхід для файлу щоб отримати об'єкт типу File. Як тільки у вас є об'єкт File
, ви можете запитати підписаний URL від нового API backend так, як ви зазвичай робите запити до API:
function getPresignedUrl(file: File) {
return makeAPIRequest(
"POST",
"upload/create",
{
original_filename: file.name,
content_type: file.type,
},
(response) => response as {
key: string;
presigned_upload_url: string;
},
);
}
Потім ви можете створити XmlHttpRequest, щоб PUT-нути файл безпосередньо в S3:
function uploadFile(
file: File,
presignedUploadUrl: string,
onProgress: (pct: number) => void,
): Promise<void> {
return new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const pct = e.loaded / e.total;
onProgress(pct * 100);
}
});
xhr.upload.addEventListener("error", (e) => {
reject(new Error("Upload failed: " + e.toString()));
});
xhr.upload.addEventListener("abort", (e) => {
reject(new Error("Upload aborted: " + e.toString()));
});
xhr.addEventListener("load", (e) => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error("Upload failed " + xhr.status));
}
});
xhr.open("PUT", presignedUploadUrl, true);
try {
xhr.
Пам'ятайте, що заголовок перекладено лише один раз і пізніше використовується як підказка.
надіслати(файл);
} catch (e) {
відхилити(new Error("Помилка завантаження: " + e.toString()));
}
});
}
Якщо ваш проект використовує React Hooks, ви можете зв'язати це все разом так:
export function використовуватиЗавантаження() {
const [станЗавантаження, встановитиСтанЗавантаження] = useState<
"пасивний" | "початок" | "завантаження" | "завершення" | "виконано" | "помилка"
>("пасивний");
const [прогресЗавантаження, встановитиПрогресЗавантаження] = useState(0);
const [помилкаЗавантаження, встановитиПомилкаЗавантаження] = useState<Error | null>(null);
return {
станЗавантаження,
прогресЗавантаження,
помилкаЗавантаження,
завантажити: async (
файл: Файл,
успішно: (ключЗавантаження: рядок) => Promise<void>,
) => {
встановитиСтанЗавантаження("початок");
try {
// Отримати підписаний URL від нашого сервера API:
const { ключ, підписаний_url_завантаження } = await отриматиПідписанийURL(
файл,
);
встановитиСтанЗавантаження("завантаження");
// Фактичне завантаження за допомогою XmlHttpRequest:
await завантажитиФайл(файл, підписаний_url_завантаження, (відсоток) => {
встановитиПрогресЗавантаження(відсоток);
});
встановитиСтанЗавантаження("завершення");
// Зробіть щось корисне з цим завантаженим файлом; ймовірно, передаєте
// цей ключ до іншої точки доступу API!
await успішно(ключ);
встановитиСтанЗавантаження("виконано");
} catch (e) {
встановитиСтанЗавантаження("помилка");
встановитиПомилкаЗавантаження(e);
}
},
};
}
Нарешті: Використання нового завантаженого файлу
Після завершення завантаження, ви можете передати ключ завантаження S3 назад до API, щоб зберегти його десь або післяпроцесувати його по-своєму бажанню.
Додайте ще одну точку доступу API, яка приймає ключ_завантаження
, який є шляхом у межах вашого лише для завантаження сховища S3. Потім ви можете завантажити і перевірити файл, або скопіювати його в інше сховище для негайного споживання вашими іншими службами.
(Я звичайно ставлю ідентифікатор аутентифікованого користувача у ключі завантаження, щоб ви могли перевірити, що завантаження відповідає поточному користувачеві всередині цієї точки доступу.)
Ось приклад на Python копіювання бакету в публічний бакет з публічною назвою:
ключ_завантаження = запит.
validated_data["upload_key"]
ext = upload_key.split(".")[-1]
slug = slugify(request.validated_data["filename"])
date = datetime.now().strftime(r"%Y%m%d_%H%M%S")
public_key = f"media/{request.user.id}/{date}-{slug}.{ext}"
спробуйте:
public_content_s3_client().копія(
CopySource={
"Bucket": "yourproject-upload",
"Key": upload_key,
},
Bucket="yourproject-public",
Key=public_key,
)
except Exception:
logging.exception(f"Не вдалося скопіювати файл для користувача = {request.user.id}")
Ось і все, сподіваюсь, що це буде корисно для когось! Знову ж таки, ось посилання на gist з усим цим кодом. 🥰
Дайте мені знати @taylorhughes, якщо у вас є які-небудь думки або відгуки.
Перекладено з: Simple & secure direct-to-S3 uploads from modern browsers