Прості та безпечні прямі завантаження у файлове сховище S3 з сучасних браузерів.

Привіт, я Тейлор Х'юз. Я є інженером-програмістом. Я працював над додатками і вибудовував команди в компаніях Facebook, Google, Clubhouse та багатьох стартапах між ними.

Проблема з надання користувачам можливості завантажувати файли в корзину S3 виникає в кожному проєкті. Однак, визначити правильні JavaScript компоненти і налаштування, щоб зробити все це працюючим разом, видається справжнім відкриттям.

Документація AWS порадила б вам налаштувати додатковий сервіс аутентифікації та імпортувати весь SDK AWS JS у ваш клієнтський код — але цього насправді не потрібно!

Замість цього, ми можемо використовувати попередньо підписані URL та сучасні веб-API, щоб легко завантажувати файли безпосередньо в S3 з браузера із лише сотні рядків коду.

Загальне рішення, з погляду браузера користувача, виглядає так:

  1. Користувач натискає "Завантажити файл" і вибирає файл
  2. З урахуванням метаданих цього файлу, запитайте попередньо підписаний URL S3 PutObject з вашого серверного API
  3. З урахуванням цього попередньо підписаного URL S3, браузер виконує PUT-запит для файла за допомогою XmlHttpRequest та може відстежувати прогрес
  4. Після завантаження, браузер повертає новий ключ назад до API знову для того, щоб викликати те, що ваше API робить із завантаженими файлами
  5. Прибуток!

Якщо ви хочете перейти до готового коду, ось gist із TypeScript фронтендом та обробником API Python як приклади.

Я піду крок за кроком у цьому пості:

Налаштування AWS

Спочатку створіть нову корзину yourproject-upload без включеного загального доступу та без політики доступу. (Я також додав правило життєвого циклу для цієї корзини, щоб всі файли всередині видалялися після 24 годин — я переміщую завантажені файли в інше місце для загального використання.)

По-друге, додайте нового користувача IAM під назвою web-upload-only. Отримайте доступ до ключа та секрета нового користувача та додайте ці облікові дані до вашого внутрішнього веб-сервера, якщо це зазвичай робиться.

(Ці дані для доступу повинні бути розділені від ваших основних облікових даних AWS.)

По-третє, дайте доступ s3:PutObject до web-upload-only для будь-яких шляхів у вашому новому кошику yourproject-upload/*. (Ми обмежимо ключі, які можна записувати, коли ми повернемо підписані URL-адреси PutObject.)

pic

Встроєна політика для користувача 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,
aws
secretaccesskey=settings.UPLOADAWSSECRETACCESSKEY,
regionname=AWSREGION,
)

@apiview("/upload/create")
def create
upload(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

Leave a Reply

Your email address will not be published. Required fields are marked *