# 上传时自动检测 MIME Source: https://bitiful-docs.mintlify.app/best-practice/auto-detect-mime 本文介绍如何用缤纷云对文件 MIME 的自动检测能力。 ## 利用缤纷云 S4 自动检测文件对象的 MIME 24 年,我们强化了 **M2TS** 和 **M4S** 视频切片类型的检测,以帮助您应对日渐严重的 **资源滥用攻击**。 在 《[在客户端中安全地上传文件](/best-practice/web-uploads)》的最佳实践中,我们的末尾的 Js 脚本片段将 Content-Type 强制置空完全是刻意为之,因为: * axios 采用 PUT 请求时会默认带上 `application/x-www-form-urlencoded` 的 `Content-Type`,而这与 S3 PUT 中对 `Content-Type` 的预期不同; * 对于 S3 协议来说,上传时主动带上文件 MIME 并赋值给 `Content-Type` 可作为元数据同时保存至 S3 后端系统当中(告诉 S3 上传的对象是何种 MIME 类型) 所以最好的做法可能是 ———— 将 `Content-Type` 指定为上传文件的 MIME 类型,例如: ```Javascript theme={null} const config = { headers: { 'Content-Type': 'image/jpeg', }, }; ``` 那么在 AWS / OSS 中最好的做法可能是:用 Javascript 程序创建有效的文件类型判断,并在请求「预签名链接」时告知服务端我们需要上传一个何种 MIME 类型的文件: ```Javascript theme={null} const url = `/s3_upload_url`; const params = {key: '1.jpg', mime: 'image/jpeg'}; ``` 在 Bitiful S4 中,我们有 **更好的** 选择: **`Bitiful S4 在接收到 Content-Type 为空的上传请求时,会自动根据后缀与字节智能地判断文件的 MIME 类型。`** > AWS S3 和 OSS 中会全部变成 "application/octet-stream" 这与 Javascript 中 file.type 只依赖后缀名的判断不同,把 MIME 推测工作交给 S4 会更可靠和真实。 而你需要做的一切就是: 1. **删除你的 MIME 判断代码** 2. **发起 Put 请求时将 Content-Type 置为 Null** # 使用 Bitifs 将缤纷云 S4 挂载到本地 Source: https://bitiful-docs.mintlify.app/best-practice/solutions/bitifs ## 目标 Bitifs BETA 是缤纷云官方推出的 S4 对象存储挂载工具,可以在 Windows、macOS 和 Linux 上利用它将对象存储映射成本地文件夹。 **本指南介绍如何设置 BitiFS 以将您 S4 挂载到本地。** 请确保您已 [注册缤纷云账号](https://console.bitiful.com/) 并完成 [实名认证](https://console.bitiful.com/users)。 ## 前置条件 * 已经 [创建](/bitiful-s4/handbook/quick-start) S4 存储桶 * 创建了子账户,并创建 access\_key * 确保为 `子账户` 赋予了 `相应存储桶` 所有权限 ## 分系统设置说明 请注意:Windows 需要提前安装 FUSE 支持 [Winfsp](https://github.com/billziss-gh/winfsp/releases) #### 下载 AMD64: [https://tools.bitiful.com/bitifs/bitifs.exe](https://tools.bitiful.com/bitifs/bitifs.exe) #### 安装使用 #### CMD 设置 AK/SK: ``` set AWS_ACCESS_KEY_ID={ak} set AWS_SECRET_ACCESS_KEY={sk} ``` #### Powershell 设置 AK/SK: ``` $env:AWS_ACCESS_KEY_ID={ak} $env:AWS_SECRET_ACCESS_KEY={sk} ``` 然后执行:`./bitifs.exe bucketname B:` 挂载 请注意:macOS 需要提前安装 FUSE 支持 [macFUSE](https://github.com/macfuse/macfuse/releases) #### 下载 Intel 芯片: [https://tools.bitiful.com/bitifs/bitifs\_macos\_amd64](https://tools.bitiful.com/bitifs/bitifs_macos_amd64) Apple 芯片: [https://tools.bitiful.com/bitifs/bitifs\_macos\_arm64](https://tools.bitiful.com/bitifs/bitifs_macos_arm64) #### 设置 AK/SK: ``` export AWS_ACCESS_KEY_ID={ak} export AWS_SECRET_ACCESS_KEY={sk} ``` 然后执行:`mkdir -p ~/bitiful-mount && ./bitifs bitifsmount ~/bitiful-mount` 挂载 #### 下载 AMD64: [https://tools.bitiful.com/bitifs/bitifs\_linux\_amd64](https://tools.bitiful.com/bitifs/bitifs_linux_amd64) ARM64: [https://tools.bitiful.com/bitifs/bitifs\_linux\_arm64](https://tools.bitiful.com/bitifs/bitifs_linux_arm64) #### 设置 AK/SK: ``` export AWS_ACCESS_KEY_ID={ak} export AWS_SECRET_ACCESS_KEY={sk} ``` 然后执行:`mkdir -p ~/bitiful-mount && ./bitifs bitifsmount ~/bitiful-mount` 挂载 热心网友制作的 Linux 一键脚本:[https://gitee.com/cncsrf/bitiful-oss](https://gitee.com/cncsrf/bitiful-oss) # 将 S4 对象存储与 Rclone 结合使用 Source: https://bitiful-docs.mintlify.app/best-practice/solutions/rclone 了解如何设置 Rclone 以将您的文件与 S4 对象存储同步。 ## 目标 Rclone 是一种备份工具,可以与各种存储后端同步,并且可以在 Windows、macOS 和 Linux 上使用。 **本指南介绍如何设置 Rclone 以将您的文件与 S4 对象存储同步。** 请确保您已 [注册缤纷云账号](https://console.bitiful.com/) 并完成 [实名认证](https://console.bitiful.com/users)。 ## 前置条件 * 已经 [创建](/bitiful-s4/handbook/quick-start) S4 存储桶 * 创建了子账户,并创建 access\_key * 确保为 `子账户` 赋予了 `相应存储桶` 所有权限 ## 说明 要配置 Rclone,请编辑或创建 `~/.config/rclone/rclone.conf` 文件并添加以下内容: ```bash theme={null} [s4-demo] type = s3 provider = Other env_auth = false access_key_id = secret_access_key = end = https://s3.bitiful.net ``` Rclone 现在可以使用了。 **命令示例** 列出所有 bucket: ```bash theme={null} $ rclone lsd s4-demo: ``` 创建新 bucket: ```bash theme={null} $ rclone mkdir s4-demo:mybucket ``` 列出 bucket 的内容: ```bash theme={null} $ rclone ls s4-demo:mybucket ``` 将 `/home/user/documents` 同步到 bucket: ```bash theme={null} $ rclone sync /home/user/documents s4-demo:我的桶 ``` 将文件 `/home/user/bitifl-logo.png` 复制到存储桶中: ```bash theme={null} $ rclone copy /home/user/bitifl-logo.png s4-demo:mybucket ``` 从存储桶下载文件 `bitifl-logo.png`: ```bash theme={null} $ rclone copy s4-demo:mybucket/bitifl-logo.png bitifl-logo.png ``` 将存储桶挂载成 `本地目录`: ```bash theme={null} $ rclone mount s4-demo:/ /LOCAL_DIR --no-modtime --no-checksum --no-seek \ --daemon \ --allow-other \ --vfs-cache-mode full \ --dir-cache-time 1m \ --buffer-size 128M \ --transfers 200 \ --checkers 200 \ --tpslimit 200 \ --vfs-read-chunk-size 10M \ --vfs-read-chunk-size-limit 256M \ --vfs-cache-max-age 24h \ --vfs-cache-max-size 1G \ --log-level INFO \ --log-file ./rclone.log \ --umask 002 \ --attr-timeout 1s \ --volname "s4mount" ``` #### 相关阅读 1. [官方 Rclone 文档](https://rclone.org/docs/)。 2. [Ugediao: Debian 服务器通过 rclone 挂载缤纷云](https://ugediao.com/debian-rclone-bitiful.html) # CDN 使用高级防盗链 Source: https://bitiful-docs.mintlify.app/best-practice/token-link 本文介绍如何在缤纷云 CDN 中使用高级防盗链。 为保护资源被滥用,CDN 配置 Referer 和 IP 的黑白名单来判断是否是合法用户及来源。然后 Referer 可被 **轻松伪造**,实际使用中仍存在隐患。 为了更好地保护源站资源,CDN 支持加密 URL 的功能,可以通过 **高级鉴权** 的方式来正确响应合法请求、拒绝非法请求。 ## 工作原理 在控制台为需要保护的 “加速项目” 启用“高级鉴权”,并设置一个名为 `鉴权 Key` 的“暗号”,利用与 CDN 同样策略的方式向合法用户提供利加密的 URL,并通过 ts 参数设定该 URL 的有效时间戳; 用户在访问资源时会根据下面的 `规则` 判断请求的合法性: * **是** - `继续判断下一条规则` * **否** - `返回 403 Forbidden`; * **是** - `继续判断下一条规则` * **否** - `返回 403 Forbidden`; * **是** - `200 并返回资源` * **否** - `返回 403 Forbidden`。 ## 典型范例 一个正确的典型高级防盗链 URL 应该类似于: > [http://tests3origin.dogecast.com/image.jpg?`_ts`=1701091625&`_btf_tk`=960edecad993db64d8aab938c7dfc3f7](http://tests3origin.dogecast.com/image.jpg?_ts=1701091625&_btf_tk=960edecad993db64d8aab938c7dfc3f7) 其中: * ts: 为 UTC 时间戳,意为:该 URL 的时效时间(**晚于该时间** 后链接将返回 403) * token: 用 **tokenKey** + **path** (本例为 /image.jpg) + **args** (本例为 \_ts=1701091625) ## 代码示例: 下面是部分主流语言实现上述 **典型范例** 中的 `高级鉴权 URL` 的示例代码。 它们应该可在各个 playground 中被正确执行。 **安全提示:** 下面虽然列出了部分客户端语言的示例,但缤纷云建议您永远 **只在服务端生成** 高级鉴权链接并分发给客户端。 因为这可以: 1. 最大程度保护 **tokenKey** 的安全; 2. 更高效、安全地随时替换 **tokenKey**(不必迫使用户更新客户端)。 ### PHP 语言版本 ```php theme={null} function creatLink() { $deadLine = time() + 60; # 链接在未来的 60秒 内有效 $tokenKey = "**testsecretkey**"; # 缤纷云后台设置的 鉴权Key $domain = "http://tests3origin.dogecast.com"; $fileName = "/image.jpg"; $md5RawString = $tokenKey.$fileName.$deadLine; $md5Result = md5($md5RawString); $tokenLink = $domain.$fileName. "?_btf_tk=".$md5Result. "&_ts=".$deadLine; return $tokenLink; } echo creatLink(); ``` ### Go 语言版本 time.Now().Unix() 有效性提示 * 大部分 Golang playground 并不支持获取时间戳 ———— time.Now().Unix() 将会得到一个错误的早期时间; * 请注意选择部分第三方支持该特性的 Golang playground; * 以下 golang 示例代码可以在 [Programiz](https://www.programiz.com/golang/online-compiler/) 得到正确地执行。 ```go theme={null} package main import ( "crypto/md5" "encoding/hex" "fmt" "time" ) func createLink() string { deadLine := time.Now().Unix() + 60 // 链接在未来的 60秒 内有效 tokenKey := "testsecretkey" // 缤纷云后台设置的 鉴权Key domain := "http://tests3origin.dogecast.com" fileName := "/image.jpg" rawString := fmt.Sprintf("%s%s%d", tokenKey, fileName, deadLine) hasher := md5.New() hasher.Write([]byte(rawString)) md5Result := hex.EncodeToString(hasher.Sum(nil)) tokenLink := fmt.Sprintf("%s%s?_btf_tk=%s&_ts=%d", domain, fileName, md5Result, deadLine) return tokenLink } func main() { fmt.Println(createLink()) } ``` ### Python3 语言版本 ```python theme={null} import hashlib import time def create_link(): deadLine = int(time.time()) + 60 # 链接在未来的 60秒 内有效 tokenKey = "testsecretkey" # 缤纷云后台设置的 鉴权Key domain = "http://tests3origin.dogecast.com" fileName = "/image.jpg" rawString = f"{tokenKey}{fileName}{deadLine}" md5Result = hashlib.md5(rawString.encode()).hexdigest() tokenLink = f"{domain}{fileName}?_btf_tk={md5Result}&_ts={deadLine}" return tokenLink print(create_link()) ``` ### JS 语言版本 ```javascript theme={null} const crypto = require('crypto'); function createLink() { let deadLine = Math.floor(Date.now() / 1000) + 60; // 链接在未来的 60秒 内有效 let tokenKey = "testsecretkey"; // 缤纷云后台设置的 鉴权Key let domain = "http://tests3origin.dogecast.com"; let fileName = "/image.jpg"; let rawString = tokenKey + fileName + deadLine; let md5Result = crypto.createHash('md5').update(rawString).digest('hex'); let tokenLink = domain + fileName + "?_btf_tk=" + md5Result + "&_ts=" + deadLine; return tokenLink; } console.log(createLink()); ``` ### Java 语言版本 ```Java theme={null} import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.security.MessageDigest; import java.nio.charset.StandardCharsets; import java.math.BigInteger; public class Main { public static void main(String[] args) throws NoSuchAlgorithmException { System.out.println(createLink()); } public static String createLink() throws NoSuchAlgorithmException { long deadline = Instant.now().getEpochSecond() + 60; // 链接在未来的 60秒 内有效 String tokenKey = "testsecretkey"; // 缤纷云后台设置的 鉴权Key String domain = "http://tests3origin.dogecast.com"; String fileName = "/image.jpg"; String rawString = tokenKey + fileName + deadline; String md5Result = getMd5(rawString); String tokenLink = domain + fileName + "?_btf_tk=" + md5Result + "&_ts=" + deadline; return tokenLink; } public static String getMd5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] messageDigest = md.digest(input.getBytes(StandardCharsets.UTF_8)); BigInteger no = new BigInteger(1, messageDigest); String hashtext = no.toString(16); while (hashtext.length() < 32) { hashtext = "0" + hashtext; } return hashtext; } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } } ``` ### rust 语言版本 ```rust theme={null} extern crate chrono; extern crate crypto; use chrono::prelude::*; use crypto::digest::Digest; use crypto::md5::Md5; fn create_link() -> String { let deadline = Utc::now().timestamp() + 60; // 链接在未来的 60秒 内有效 let token_key = "testsecretkey"; // 缤纷云后台设置的 鉴权Key let domain = "http://tests3origin.dogecast.com"; let file_name = "/image.jpg"; let raw_string = format!("{}{}{}", token_key, file_name, deadline); let mut hasher = Md5::new(); hasher.input_str(&raw_string); let md5_result = hasher.result_str(); let token_link = format!("{}{}?_btf_tk={}&_ts={}", domain, file_name, md5_result, deadline); token_link } fn main() { println!("{}", create_link()); } ``` **请注意: 在运行 Rust 代码前需要在你的 Cargo.toml 文件中添加以下依赖** ```toml theme={null} [dependencies] chrono = "0.4" rust-crypto = "0.2" ``` ### swift 语言版本 ```swift theme={null} import CommonCrypto import Foundation func MD5(string: String) -> String { let length = Int(CC_MD5_DIGEST_LENGTH) var digest = [UInt8](repeating: 0, count: length) if let data = string.data(using: String.Encoding.utf8) { data.withUnsafeBytes { _ = CC_MD5($0.baseAddress, CC_LONG(data.count), &digest) } } return digest.map { String(format: "%02x", $0) }.joined() } func createLink() -> String { let deadline = Int(Date().timeIntervalSince1970) + 60 let tokenKey = "testsecretkey" let domain = "http://tests3origin.dogecast.com" let fileName = "/image.jpg" let rawString = "\(tokenKey)\(fileName)\(deadline)" let md5Result = MD5(string: rawString) let tokenLink = "\(domain)\(fileName)?_btf_tk=\(md5Result)&_ts=\(deadline)" return tokenLink } print(createLink()) ``` ### kotlin 语言版本 ```kotlin theme={null} import java.security.MessageDigest import java.time.Instant fun createLink(): String { val deadline = Instant.now().epochSecond + 60 val tokenKey = "testsecretkey" val domain = "http://tests3origin.dogecast.com" val fileName = "/image.jpg" val rawString = "$tokenKey$fileName$deadline" val md = MessageDigest.getInstance("MD5") md.update(rawString.toByteArray()) val md5Result = md.digest().joinToString("") { "%02x".format(it) } val tokenLink = "$domain$fileName?_btf_tk=$md5Result&_ts=$deadline" return tokenLink } fun main() { println(createLink()) } ``` # 在客户端中安全地上传文件 Source: https://bitiful-docs.mintlify.app/best-practice/web-uploads 本文介绍如何用预签名 URL 安全地让客户端端具备高性能的大文件上传能力。 ## 为何不建议直接用 Javascript SDK 等客户端 SDK 来上传文件? 1. 容易被反编译后暴露 ak / sk 2. 不够灵活 安全建议:永远不要使用 API 下发 ak / sk ## 更好的方案 ### 后端生成「预签名链接」并通过 API 下发 前端可以对预签名链接直接发起 PUT 请求,这样我们可以收获的好处有: 1. 更安全:永远不会因 `暴露客户端代码` 或 `被反编译` 而泄露至关重要的 AccessKey 和 SecretKey; 2. 更灵活:利用显式指定 `Content-Type` 和 `Content-Length` 来防止客户端被破解,从而绕过业务策略的限制 3. 低成本:统一由服务端传递预签名链接,客户端开发人员只需要完成 PUT HTTP 请求即可,不需要学习和调试不同语言下的 S3 SDK(事实证明,AWS SDK不同的语言版本由不同的团队维护,其规范、特性差异较大,不利于项目稳定。) 4. 高性能:上传请求依然是直接对 缤纷云 S4 发起,不需要服务端中转。 ### 如何运作 让后端程序(golang、python、php、nodejs)调用相应的 SDK 生成一个具有 PutObject 方法效力的「预签名」链接传递给 Web 页面的 Javascript 脚本。 Javascript 脚本利用 网络请求组件(如 axios)来将本地文件 PUT 到上面提到的「预签名 PutObject 链接」 缤纷云开源了全功能预签名 API 本地部署项目。它无状态,可以作为微服务快速结合到当前的系统中。 Github: [https://github.com/bitiful/s3-presigned-api-server](https://github.com/bitiful/s3-presigned-api-server) ### 不同语言后端接口生成 PutObject 预签名链接示例: ```python main.py theme={null} import boto3 from flask import Flask from flask import render_template_string from flask import request app = Flask(__name__) @app.get('/s3_upload_url') def get_upload_url(): # Config s3endpoint = 'https://s3.bitiful.net' # 请填入控制台 “Bucket 设置” 页面底部的 “Endpoint” 标签中的信息 s3region = 'cn-east-1' s3accessKeyId = '<--子账户 accessKey-->' # 请到控制台创建子账户,并为子账户创建相应 accessKey s3SecretKeyId = '<--子账户 secretKey-->' # !!切记,创建子账户时,需要手动为其分配具体权限!! # 连接 S3 client = boto3.client( 's3', aws_access_key_id = s3accessKeyId, aws_secret_access_key = s3SecretKeyId, endpoint_url = s3endpoint, region_name = s3region ) url = client.generate_presigned_url( 'put_object', Params={ 'Bucket': '<--缤纷云存储桶名-->', 'Key': request.args.get('key'), }, ExpiresIn=3600 ) return {'url': url} ``` ```javascript main.js theme={null} // 参考 1:https://juejin.cn/post/7019216042220077092 // 参考 2:https://northflank.com/guides/connect-nodejs-to-minio-with-tls-using-aws-s3 const { S3Client } = require("@aws-sdk/client-s3"); const s3Client = new S3Client({ endpoint: "https://s3.bitiful.net", credentials: { accessKeyId: "<--子账户 accessKey-->", secretAccessKey: "<--子账户 secretKey-->", } }); //引入相关模块 const { PutObjectCommand, } = require("@aws-sdk/client-s3"); const { getSignedUrl } = require("@aws-sdk/s3-request-presigner"); app.get("/s3_upload_url", (req, res, next) => { //初始化命令实体 const putCmd = new PutObjectCommand({ Bucket: "<--缤纷云存储桶名-->", Key: req.query.key }); //获取签名 getSignedUrl(s3Client, putCmd, { expiresIn: 3600 }).then((url) => { //将签名好的url回传给前台 res.send(url); next(); }); }); ``` ```rust main.rs theme={null} // 参考:https://developer.qiniu.com/kodo/12572/aws-sdk-rust-examples // [dependencies] // actix-web = "4" // serde = { version = "1.0", features = ["derive"] } // serde_json = "1.0" // anyhow = "1.0.75" // aws-config = "1.3.0" // aws-credential-types = { version = "1.2.0", features = ["hardcoded-credentials"] } // aws-sdk-s3 = "1.25.0" // aws-sdk-sts = "1.22.0" // aws-smithy-types = { version = "1.1.8", features = ["rt-tokio"] } // tokio = { version = "1.32.0", features = ["full"] } use actix_web::{web, App, HttpResponse, HttpServer, Responder}; use aws_config::{BehaviorVersion, SdkConfig}; use aws_credential_types::{provider::SharedCredentialsProvider, Credentials}; use aws_sdk_s3::{config::Region, presigning::PresigningConfig, Client}; use std::time::Duration; async fn get_upload_url(query: web::Query) -> impl Responder { let key = query.get("key").and_then(|v| v.as_str()).unwrap_or(""); let url = get_url(key); let response_data = serde_json::json!({ "url": url.await.unwrap_or("".to_string())}); HttpResponse::Ok() .content_type("application/json") // 设置响应的 Content-Type .json(response_data) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/s3_upload_url", web::get().to(get_upload_url)) }) .bind("127.0.0.1:8080")? .run() .await } async fn get_url(key: &str) -> anyhow::Result { let client = Client::new( &SdkConfig::builder() .region(Region::new("cn-east-1")) .endpoint_url("https://s3.bitiful.net") .behavior_version(BehaviorVersion::v2023_11_09()) .credentials_provider(SharedCredentialsProvider::new(Credentials::from_keys( "<--子账户 accessKey-->", "<--子账户 secretKey-->", None, ))) .build(), ); let request = client .put_object() .bucket("<--缤纷云存储桶名-->") .key(key) .presigned( PresigningConfig::builder() .expires_in(Duration::from_secs(36000)) .build()?, ) .await?; Ok(request.uri().to_string()) } ``` ```go main.go theme={null} // go get github.com/aws/aws-sdk-go package main import ( "encoding/json" "log" "net/http" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" ) const ( bucket = "<--缤纷云存储桶名-->" endpoint = "https://s3.bitiful.net" region = "cn-east-1" accessKey = "<--子账户 accessKey-->" secretKey = "<--子账户 secretKey-->" ) func main() { http.HandleFunc("/s3_upload_url", handleS3UploadURL) log.Println("Server started at http://localhost:8080") log.Fatal(http.ListenAndServe(":8080", nil)) } func handleS3UploadURL(w http.ResponseWriter, r *http.Request) { // Parse query parameter keys, ok := r.URL.Query()["key"] if !ok || len(keys[0]) < 1 { http.Error(w, "Missing 'key' query parameter", http.StatusBadRequest) return } key := keys[0] // Create AWS session sess := session.Must(session.NewSession(&aws.Config{ Region: aws.String(region), Endpoint: aws.String(endpoint), Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""), })) // Create S3 service client svc := s3.New(sess) // Generate the URL req, _ := svc.PutObjectRequest(&s3.PutObjectInput{ Bucket: aws.String(bucket), Key: aws.String(key), }) urlStr, err := req.Presign(15 * time.Minute) // URL expires in 15 minutes if err != nil { http.Error(w, "Failed to sign request: "+err.Error(), http.StatusInternalServerError) return } // Create JSON response response := struct { URL string `json:"url"` }{ URL: urlStr, } jsonResponse, err := json.Marshal(response) if err != nil { http.Error(w, "JSON encoding error: "+err.Error(), http.StatusInternalServerError) return } // Set Content-Type and write the response w.Header().Set("Content-Type", "application/json") w.Write(jsonResponse) } ``` ```php main.php theme={null} # composer require aws/aws-sdk-php require 'vendor/autoload.php'; use Aws\S3\S3Client; use Aws\Exception\AwsException; // 配置 $bucket = '<--缤纷云存储桶名-->'; $region = 'cn-east-1'; $keyId = '<--子账户 accessKey-->'; $secret = '<--子账户 secretKey-->'; $endpoint = 'https://s3.bitiful.net'; // 创建 S3 客户端 $s3Client = new S3Client([ 'version' => 'latest', 'region' => $region, 'credentials' => [ 'key' => $keyId, 'secret' => $secret, ], 'endpoint' => $endpoint, 'use_path_style_endpoint' => true, ]); // 检查是否有 key 参数 if (!isset($_GET['key']) || empty($_GET['key'])) { header('Content-Type: application/json'); echo json_encode(['error' => 'Missing key parameter']); http_response_code(400); exit; } $key = $_GET['key']; // 生成预签名 URL try { $cmd = $s3Client->getCommand('PutObject', [ 'Bucket' => $bucket, 'Key' => $key ]); $request = $s3Client->createPresignedRequest($cmd, '+20 minutes'); // 获取预签名 URL $presignedUrl = (string) $request->getUri(); header('Content-Type: application/json'); echo json_encode(['url' => $presignedUrl]); } catch (AwsException $e) { header('Content-Type: application/json'); echo json_encode(['error' => $e->getMessage()]); http_response_code(500); } ``` ### 前端上传示例: ```html upload_demo.web.html theme={null} S3 File Upload

S3 File Upload

``` 上面的 Javascript 脚本在发起 Put 请求时将「Content-Type」设置为空值,建议使用缤纷云独有的 [MIME 自动检测](/best-practice/auto-detect-mime) 功能。 # 主要名词与概念 Source: https://bitiful-docs.mintlify.app/bitiful-s4/definitions 这里列出和定义一些S4对象存储服务中遇到的概念。 缤纷云 S4 的存储桶均为 **目录桶**,约相当于 Amazon AWS 最新推出的:[Directory Buckets](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/directory-buckets-overview.html)。 > AWS 基于它构建了最新的高性能 S3 产品:[S3 Express One Zone](https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/s3-express-one-zone.html) ## 存储桶(Bucket) * 存储桶是您存储内容对象(Object)的独享空间,所有的对象都必须隶属于某个存储桶。存储桶具有各种配置属性,包括地域、访问权限等。 * 存储桶的名字全局唯一,如果您创建了某个名称的存储桶,其他用户将无法再创建同名的存储桶。 * 每一个桶的存储空间没有限制。 * 存储桶命名规范如下: * 只能包括小写字母 a \~ z、数字 0 \~ 9 和 半角横线(-)。 * 开头和结尾只能是数字或字母。 * 长度必须在 4 \~ 63 个字符之间。 ## 对象(Object) 对象是 S4 存储数据的基本单元,在本文档中 对象、文件 与 Object 意义相同。对象由键(Key),数据(Data)和元数据(Metadata)三部分组成。 * Key:为在某个存储桶(Bucket)中一个对象(文件)的唯一标识,它包含目录、后缀的完整路径。存储桶名 + Key 组成该对象在互联网上访问与操作的 [URL](https://baike.baidu.com/item/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E5%AE%9A%E4%BD%8D%E7%B3%BB%E7%BB%9F); * Data:为该数据的实体内容,例如:一张图片中; * Metadata:与本地文件系统不同,对象存储独有可描述一个对象(文件)其他属性的“元数据”。 * 对象的命名规范如下: * 使用 UTF-8 编码。 * 长度必须在 1~696 个字符之间。 * 不能以反斜线(\)开头。 * 不支持 \a、\b、\t、\n、\v、\f、\r 字符。 * 不支持 ASCII 0 \~ 31 和 127 的不可见字符 * 目录对象 vs 文件对象 * S4 特有对象类型,旨在为高性能场景带来原生的 Rename 等特性; * 目录对象只能以斜杠(/)结尾; * 目录对象没有实质的 Data 二进制数据(Body 为空); - **存储桶(Bucket)名为**:somebucket; - **Key 为**:userFiles/000001.avatar; - **数据(Data)为**:该图片实际的二进制内容; - **元数据(Metadata)为**:image/jpeg。在对象存储中,不需要依靠[文件扩展名](https://baike.baidu.com/item/%E6%96%87%E4%BB%B6%E6%89%A9%E5%B1%95%E5%90%8D/1270230)来判断文件类型(MIME),只要元数据(Metadata)中指定了其类型为“image/jpeg”,那么在访问时浏览器可自动识别为图片。 ## 地域(Region) 地域表示 S4 的数据中心所在可用区的地理位置,目前只有中国东部1可用区(cn-east-1)。 ## 子账户(Sub-user) 子账户是缤纷云最小的权限管理单位,每个子账户可以单独设置不同`存储桶`的权限,每个子账户下的 AccessKey 集成该子账户所有的权限。 ## 访问密钥(AccessKey) AccessKey 简称AK,是 AccessKey Id 和 AccessKey Secret 的统称,凭借 AccessKey,您可以在任何兼容 S3 协议的第三方工具、SDK 中方便地使用 缤纷云 S4。 # 图像 BlurHash 与 ThumbHash 哈希占位技术: Source: https://bitiful-docs.mintlify.app/bitiful-s4/features/hash-placeholder BlurHash 和 ThumbHash 为图像提供惊艳又轻量的预加载效果,让图片访问体验更上三层楼! 传统的图像加载方式往往会导致用户等待或看到空白页面。BlurHash 和 ThumbHash 通过轻量级哈希值预先生成模糊占位图,为用户提供即时反馈,大幅提升加载体验。 缤纷云 **CoreIX 原生支持** 获取 [BlurHash](https://blurha.sh/) 和 [ThumbHash](https://evanw.github.io/thumbhash/) 两种图像占位哈希技术,通过 [fmt=blurhash](/coreix/features/fmt) 或 [fmt=thumbhash](/coreix/features/fmt) 参数即可获取。 ## 图像预加载占位方案对比 | **特性** | **高斯模糊图** | **BlurHash** | **ThumbHash** | **主色值** | | :----- | :--------------------------- | :-------------------------- | :--------------------------- | :-------------------------- | | 占数据库容量 | 较大 ( 200 \~ 5k 字节) | 极小 (30+ 字节) | 极小 (20+ 字节) | 最短 (7-9 字符) | | 模糊质量 | 最高 | 较高 | 很高 | 无任何细节 | | 性能影响 | 数据库性能影响较大 | 极小 | 极小 | 极小 | | 综合评价 | 模糊质量最好
存储容量很大
效率一般 | 模糊质量较好
存储容量小
效率很高 | 模糊质量很好
存储容量更小
效率很高 | 纯色无细节
存储容量最小
效率最高 | 我们非常感谢 [BlurHash](https://evanw.github.io/thumbhash/) 和 [ThumbHash](https://blurha.sh/) 的开源,但原始库对图片格式支持 **非常有限**,更无法简单支持 **视频**、**动图**。得益于缤纷云 CoreIX 绝佳的 **视频**、**图片格式** 和 **色彩模式** 兼容能力,你可以简单我们的接口在 0.1 秒内获得大量的 **额外兼容性**,包括: 1. P3、Adobergb、CMYK 等色域或色彩模式的照片; 2. 大量 RAW 格式原片; 3. 借助 [frame 截图参数](/coreix/features/frame) 支持获取 `视频` 和 `动图` 的哈希占位图 ## 方案预览 blurhash-sample BlurHash 示例:左侧为原图,右侧为其 BlurHash 生成的模糊占位图 ## 如何使用 ### 原理与步骤 1. 只需在图像URL后添加相应参数: * BlurHash: `fmt=blurhash` * ThumbHash: `fmt=thumbhash` 2. 将得到的 `占位哈希值` 直接存入数据库 3. 在客户端请求数据时,直接将 `占位哈希值` 输出到接口结果(例如 json)中 4. 客户端根据接口中相关字段提供的 `占位哈希值` Decode 出模糊图片,填充至图片容器(例如 html 中的 img 标签)—— 这一步无需网络,处理时间大约是亚毫秒级,用户看到预加载的占位模糊图不会有任何延迟感 5. 再通过相关 URL 远程加载清晰的缩略图或原图 结合 crop 和 resize(图像缩略、裁剪)参数,可获取原图某一部分的模糊hash # 签名URL请求次数限制 Source: https://bitiful-docs.mintlify.app/bitiful-s4/features/max-requests-limits S4 原生支持 x-bitiful-max-requests 限制签名后 URL 的访问次数。 传统的对象存储(Tencent COS、Aliyun OSS、AWS S3)的预签名 URL 只支持时间限制,不支持次数限制,这大大降低了对象存储的安全性。 缤纷云 S4 支持对 GetObject 和 PutObject 的预签名进行次数限制。 ## 如何使用 只需在生成预签名 URL 时,加入 `x-bitiful-max-requests=10` 参数(可将后面的 10 修改成你需要限制的次数) 系统会在第 11 次访问的时候返回 403 ReachTheMaxRequests 错误。 安全建议 为 PutObject 签名时,将时间限制在较小范围(如 10分钟),并限制最大请求次数为:1 次,这样可以帮助 PutObject 的签名 URL 杜绝被重放。 ## 范例参考 缤纷云开源了一个无状态的 签名URL 生成服务,可支持多种安全参数及独有的性能特性。 | **参数名** | **类型** | **必填** | **默认值** | **说明** | **作用范围** | | :--------------- | :------ | :----- | :------ | :-------------------------------- | :------------ | | key | String | 是 | - | 对象的键名/路径,用于在S3存储桶中标识对象 | `PUT` & `GET` | | content-length | Int | 否 | 不限制 | 上传内容的长度(字节),范围:大于0且不超过1GB(1024MB) | `PUT` | | no-wait | Int | 否 | 0 | 开启"即传即收"功能的等待超时时间(秒),最大值为10秒 | `GET` | | **max-requests** | **Int** | **否** | **0** | **最大下载次数限制,指定URL可被访问的最大次数** | `PUT` & `GET` | | expire | Int | 否 | 3600 | URL的有效期(秒),默认为1小时 | `PUT` & `GET` | | force-download | Bool | 否 | false | 是否强制下载(设置为true时会添加attachment响应头) | `GET` | | limit-rate | Int | 否 | 0 | 单线程限速值(字节/秒) | `GET` | Github:[https://github.com/bitiful/s3-presigned-api-server](https://github.com/bitiful/s3-presigned-api-server) ## 项目集成 缤纷云开源的「**缤纷快传**」较好地利用了这一特性 预览:[https://send.bitiful.com/](https://send.bitiful.com/) 开源地址:[https://github.com/bitiful/send](https://github.com/bitiful/send) # 移动/重命名对象 Source: https://bitiful-docs.mintlify.app/bitiful-s4/features/move-object S4 原生支持 Move 和 Rename 操作 传统的对象存储(Tencent COS、Aliyun OSS、AWS S3)均不支持原生的 Move 操作,在 Rename 场景需要先 Copy 对象 再 Delete 对象,开销较大。 缤纷云 S4 支持原生的 Move 操作,可以对文件进行 **高效的 移动 或 重命名 操作**。 # 上下行同步传输技术 Source: https://bitiful-docs.mintlify.app/bitiful-s4/features/simul-transfer 利用 Simul-Transfer 同步传输技术,让文件分享效率提升 100% ## 介绍 ### 什么是 Simul-Transfer 同步传输 缤纷云基于自身独有的流式架构,首次为 **「对象存储」** 原生带来了全新的 **「同步传输技术」Simul-Transfer**。它让分享的文件在 **上传开始的一瞬间便可以被下载**,同样的带宽环境下,最多可 **节省1倍的传输时间**。 ### 性能如何 它原生植根于缤纷云独有的流式架构中,所以 * **没有任何性能损失**:从上传开始的那一刻开始,便支持 `与普通对象同等的并发、速率水平`\[1] * **没有任何的功能妥协**:完整支持 缤纷云 S4 `所有的功能特性`\[2],如:防盗链、预签名、Range 请求 等。 ### 如何运作 * 传统的对象存储会在文件写入时不会有任何动作,**写入完毕之后,元数据才会感知**,进而对下载请求做出响应。 * 缤纷云新一代对象存储,从上传第一个字节开始,底层的流式架构便可让全局感知,从而让用户从 **上传的第一秒即可开始下载**。 🤔 **without Simul-Transfer: 必须等待上传结束后才能开始下载**