Lập trình viên / Tra cứu
API Storage cửa hàng: tải tiếp theo phần
Tải file độc lập vào cửa hàng, tiếp tục sau mất mạng và kiểm tra trạng thái quét.
Chọn đúng API và quyền
Base URL: https://app.whypik.com/api/v1/storage/SHOP_UUID. Dùng Authorization: Bearer với API key đọc/ghi tạo trong Storage cửa hàng. Đây là khóa server, không nhúng trong web công khai. Mọi phần và lệnh complete phải dùng đúng key đã tạo lượt tải; key bị thu hồi hoặc hết hạn không còn ghi được.
API này tạo file độc lập, không tạo bản phát hành sản phẩm. File mặc định riêng tư, phải quét sạch trước khi tải. Object Storage dùng Cloud API riêng và không chạy cơ chế license cửa hàng.
Hợp đồng chunks-v1
POST /files nhận filename, size (byte), sha256 (64 ký tự hex thường), transport="chunks-v1" cùng Idempotency-Key UUID. Kết quả {upload}: id, state, metadata, expires_at, release_id, transport, chunk_size, chunk_count, parts[{index,size,sha256}]. Chỉ nhận file 1 byte đến giới hạn upload_max_bytes của cửa hàng, tối đa vận hành 100 MiB; quota tài khoản vẫn áp dụng.
GET /uploads/{id} hoặc POST lặp lại cùng key và metadata trả trạng thái đã lưu. PUT /uploads/{id}/parts/{index} gửi bytes, Content-Type: application/octet-stream, Content-Length và X-Chunk-SHA256. Index bắt đầu 0; mọi phần đúng chunk_size trừ phần cuối. Cùng index/size/hash trả thành công; khác dữ liệu trả 409. Không gửi song song nhiều phần trong cùng lượt.
POST /uploads/{id}/complete với {} ghép và xác minh toàn file, trả {file}. Lặp complete không tạo file hay lượt quét mới. GET /files hiển thị ready/scan_status; ready=false chưa tải được. File sản phẩm còn tuân thủ bước công bố và license.
Lượt nhận và complete có thể trả 409 khi một tiến trình khác giữ khóa; đọc trạng thái và thử lại sau khi kết nối trước kết thúc. Hết 48 giờ không nhận phần mới. DELETE /uploads/{id} yêu cầu hủy; chủ cửa hàng có thể hủy từ Dashboard kể cả khi key cũ không còn dùng được. Dung lượng chỉ trả sau dọn. Không tự tạo key/idempotency UUID mới sau lỗi mạng.
Bỏ transport vẫn dùng giao thức nguyên file cũ: POST /files rồi PUT /uploads/{id}, thời hạn 30 phút. Không chuyển giao thức giữa chừng. Phần tải dở chỉ tiếp tục bằng cùng file và cùng metadata; đổi file cần hủy lượt cũ hoặc tạo lượt mới có đủ quota.
Ví dụ Python chạy được
Python 3.11+, cài requests. Lưu mã thành store_upload.py. Đặt WHYPIK_STORAGE_KEY trong biến môi trường; chạy python store_upload.py --shop SHOP_UUID --file ./data.zip --idempotency-key UUID. Tạo UUID một lần bằng python -c "import uuid; print(uuid.uuid4())", ghi lại và dùng nguyên UUID khi chạy lại trong 48 giờ. Mã không ghi API key ra file hoặc log.
"""Python 3.11+; pip install requests. Set WHYPIK_STORAGE_KEY in the environment.
Upload an independent Store file. Reuse the same UUID/file/key to resume for 48h.
This does not publish a software release or bypass its scan/license policy.
"""
import argparse
import hashlib
import os
import uuid
from pathlib import Path
import requests
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--shop", required=True, type=uuid.UUID)
parser.add_argument("--file", required=True, type=Path)
parser.add_argument("--idempotency-key", required=True, type=uuid.UUID)
args = parser.parse_args()
base = f"https://app.whypik.com/api/v1/storage/{args.shop}"
with requests.Session() as session:
session.headers["Authorization"] = "Bearer " + os.environ["WHYPIK_STORAGE_KEY"]
def call(method, suffix, **kwargs):
response = session.request(
method, base + suffix, timeout=(10, 190), allow_redirects=False, **kwargs
)
if response.status_code not in {200, 201}:
raise RuntimeError(f"HTTP {response.status_code}; keep the same UUID to retry.")
return response.json()
with args.file.open("rb") as stream:
sha = hashlib.file_digest(stream, "sha256").hexdigest()
row = call("POST", "/files", headers={"Idempotency-Key": str(args.idempotency_key)}, json={
"filename": args.file.name, "size": args.file.stat().st_size,
"sha256": sha, "transport": "chunks-v1",
})["upload"]
path = f"/uploads/{uuid.UUID(row['id'])}"
if row["state"] == "completed":
print("Already received; inspect scan state:", row["release_id"])
return
if row["state"] not in {"pending", "receiving"}:
raise RuntimeError("Reservation closed; inspect it before creating another.")
size = row["chunk_size"]
if type(size) is not int or not 1 <= size <= 8 * 1024**2:
raise RuntimeError("Invalid server chunk size")
if row["state"] == "pending":
saved = {part["index"]: part for part in row["parts"]}
with args.file.open("rb") as stream:
index = 0
while body := stream.read(size):
digest = hashlib.sha256(body).hexdigest()
if index in saved:
if saved[index]["size"] != len(body) or saved[index]["sha256"] != digest:
raise RuntimeError("File differs from accepted bytes")
else:
call("PUT", path + f"/parts/{index}", data=body, headers={
"Content-Type": "application/octet-stream", "X-Chunk-SHA256": digest,
})
index += 1
result = call("POST", path + "/complete", json={})["file"]
print("Received; still requires a clean scan:", result["id"])
if __name__ == "__main__":
main()