鴻蒙環(huán)境服務(wù)端簽名直傳
通過服務(wù)端簽名實(shí)現(xiàn)鴻蒙應(yīng)用直傳OSS使用戶可以通過鴻蒙端直接使用PutObject接口上傳文件到OSS,此過程通過在服務(wù)端實(shí)現(xiàn)URL簽名機(jī)制確保上傳的安全性。
方案概覽
使用鴻蒙環(huán)境實(shí)現(xiàn)文件上傳的過程如下:
要實(shí)現(xiàn)鴻蒙應(yīng)用直傳OSS,只需3步:
配置OSS:在OSS控制臺(tái)上創(chuàng)建一個(gè)Bucket,用于存儲(chǔ)用戶上傳的文件。
配置服務(wù)端:在服務(wù)端創(chuàng)建一個(gè)實(shí)例,用于從STS服務(wù)獲取一個(gè)臨時(shí)訪問憑證,然后使用臨時(shí)訪問憑證生成簽名 URL用于授權(quán)用戶在一定時(shí)間內(nèi)進(jìn)行文件上傳。
配置鴻蒙客戶端:在鴻蒙客戶端,實(shí)現(xiàn)從ECS獲取簽名并構(gòu)造PutObject請(qǐng)求,將文件上傳到OSS。
示例代碼
以下代碼示例只給出關(guān)鍵邏輯的代碼片段,完整示例工程請(qǐng)參見:oss-js-sdk-harmony-demo.zip
服務(wù)端生成簽名URL:
const express = require("express");
const mime = require("mime");
const OSS = require("ali-oss");
const app = express();
const port = 3000; // 監(jiān)聽端口
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.post("/get_sign_url", async (req, res) => {
const {
fileName,
method,
headers = {},
queries = {},
additionalHeaders = [],
} = req.body; // 從body中解析出數(shù)據(jù)
const client = new OSS({
region: "yourRegion",
accessKeyId: "yourstsAccessKey",
accessKeySecret: "yourstsAccessKeySecret",
stsToken: "yourSTSToken",
bucket: "yourBucket",
authorizationV4: true,
});
const reqHeaders = {
...headers,
};
// 處理一下content-type
if (fileName && method === "PUT") {
const fileNameSplit = fileName.split(".");
reqHeaders["content-type"] = mime.getType(
fileNameSplit.length > 1 ? fileNameSplit[fileNameSplit.length - 1] : ""
);
}
// 生成V4簽名URL
const url = await client.signatureUrlV4(
method,
300,
{
headers: reqHeaders,
queries,
},
fileName,
additionalHeaders
);
res.json({
url,
contentType: reqHeaders["content-type"],
});
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
在客戶端獲取簽名并使用簽名URL上傳文件:
import { http } from '@kit.NetworkKit';
import fs from '@ohos.file.fs';
import { request } from './request';
const serverUrl = 'http://x.x.x.x:3000/get_sign_url'; // 獲取簽名URL的服務(wù)器URL
/**
* getSignUrl返回?cái)?shù)據(jù)
*/
export interface ISignUrlResult {
/** 簽名URL */
url: string;
/** content-type */
contentType?: string;
}
/**
* 獲取簽名URL
* @param fileName 文件名稱
* @param req 用于生成V4簽名URL的請(qǐng)求信息
* @param req.method 請(qǐng)求方式
* @param [req.headers] 請(qǐng)求頭
* @param [req.queries] 請(qǐng)求查詢參數(shù)
* @param [req.additionalHeaders] 加簽的請(qǐng)求頭
*/
const getSignUrl = async (fileName: string, req: {
method: 'GET' | 'POST' | 'PUT';
headers?: Record<string, string | number>;
queries?: Record<string, string>;
additionalHeaders?: string[];
}): Promise<ISignUrlResult> => {
console.info('in getSignUrl');
try {
const response = await request(serverUrl, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json'
},
extraData: {
fileName,
method: req.method,
headers: req.headers,
queries: req.queries,
additionalHeaders: req.additionalHeaders
},
expectDataType: http.HttpDataType.OBJECT
}, 200);
const result = response.result as ISignUrlResult;
console.info('success getSignUrl');
return result;
} catch (err) {
console.info('getSignUrl request error: ' + JSON.stringify(err));
throw err;
}
};
/**
* PutObject
* @param fileUri 文件URI
*/
const putObject = async (fileUri: string): Promise<void> => {
console.info('in putObject');
const fileInfo = await fs.open(fileUri, fs.OpenMode.READ_ONLY);
const fileStat = await fs.stat(fileInfo.fd);
let signUrlResult: ISignUrlResult;
console.info('file name: ', fileInfo.name);
try {
// 獲取PutObject的簽名URL
signUrlResult = await getSignUrl(fileInfo.name, {
method: 'PUT',
headers: {
'Content-Length': fileStat.size
},
additionalHeaders: ['Content-Length']
});
} catch (e) {
await fs.close(fileInfo.fd);
throw e;
}
const data = new ArrayBuffer(fileStat.size);
await fs.read(fileInfo.fd, data);
await fs.close(fileInfo.fd);
try {
// 使用PutObject方法上傳文件
await request(signUrlResult.url, {
method: http.RequestMethod.PUT,
header: {
'Content-Length': fileStat.size,
'Content-Type': signUrlResult.contentType
},
extraData: data
}, 200);
console.info('success putObject');
} catch (err) {
console.info('putObject request error: ' + JSON.stringify(err));
throw err;
}
};
export {
getSignUrl,
putObject
};
操作步驟
步驟一:配置OSS
創(chuàng)建Bucket
創(chuàng)建一個(gè)OSS Bucket,用于存儲(chǔ)Web應(yīng)用在瀏覽器環(huán)境中直接上傳的文件。
在左側(cè)導(dǎo)航欄,單擊Bucket 列表,然后單擊創(chuàng)建 Bucket。
在創(chuàng)建 Bucket面板,選擇快捷創(chuàng)建,按如下說明配置各項(xiàng)參數(shù)。
參數(shù)
示例值
Bucket名稱
web-direct-upload
地域
華東1(杭州)
點(diǎn)擊完成創(chuàng)建。
步驟二:配置服務(wù)端
一、創(chuàng)建一臺(tái)ECS并綁定角色
操作一:創(chuàng)建ECS實(shí)例
請(qǐng)您進(jìn)入自定義購買頁面,并根據(jù)如下各模塊的內(nèi)容,創(chuàng)建或選擇購買ECS實(shí)例所需的基礎(chǔ)資源。
選擇地域 & 付費(fèi)類型
根據(jù)業(yè)務(wù)需求,選擇合適的付費(fèi)類型。本文選擇按量付費(fèi)模式,此模式操作相對(duì)靈活。
基于業(yè)務(wù)場景對(duì)時(shí)延的要求,選擇地域。通常來說離ECS實(shí)例的物理距離越近,網(wǎng)絡(luò)時(shí)延越低,訪問速度越快。本文以選擇華東1(杭州)為例。
創(chuàng)建專有網(wǎng)絡(luò)VPC & 交換機(jī)
創(chuàng)建VPC時(shí),請(qǐng)您選擇和ECS相同的地域,并根據(jù)業(yè)務(wù)需求規(guī)劃網(wǎng)段。本文以創(chuàng)建華東1(杭州)地域的VPC和交換機(jī)為例。創(chuàng)建完畢后返回ECS購買頁,刷新并選擇VPC及交換機(jī)。
說明創(chuàng)建VPC時(shí),可同時(shí)創(chuàng)建交換機(jī)。
選擇規(guī)格 & 鏡像
選擇實(shí)例的規(guī)格及鏡像,鏡像為實(shí)例確定安裝的操作系統(tǒng)及版本。本文選擇的實(shí)例規(guī)格為
ecs.e-c1m1.large
,在滿足測試需求的同時(shí),價(jià)格較為實(shí)惠。鏡像為公共鏡像Alibaba Cloud Linux 3.2104 LTS 64位
。
選擇存儲(chǔ)
為ECS實(shí)例選擇系統(tǒng)盤,并按需選擇數(shù)據(jù)盤。本文實(shí)現(xiàn)簡單Web系統(tǒng)搭建,只需要系統(tǒng)盤存儲(chǔ)操作系統(tǒng),無需數(shù)據(jù)盤。
綁定公網(wǎng)IP
本實(shí)例需要支持公網(wǎng)訪問。為了簡化操作,本文選擇直接為實(shí)例分配公網(wǎng)IP。您也可以在創(chuàng)建實(shí)例后,為實(shí)例綁定彈性公網(wǎng)IP,具體操作,請(qǐng)參見將EIP綁定至ECS實(shí)例。
說明若未綁定公網(wǎng)IP,將無法使用SSH或RDP通過公網(wǎng)直接訪問實(shí)例,也無法通過公網(wǎng)驗(yàn)證實(shí)例中Web服務(wù)的搭建。
本文選擇按使用流量的帶寬計(jì)費(fèi)模式。此模式只需為所消耗的公網(wǎng)流量付費(fèi)。更多信息,請(qǐng)參見公網(wǎng)帶寬計(jì)費(fèi)。
創(chuàng)建安全組
為實(shí)例創(chuàng)建安全組。安全組是一種虛擬網(wǎng)絡(luò)防火墻,能夠控制ECS實(shí)例的出入流量。創(chuàng)建時(shí),需要設(shè)置放行以下指定端口,便于后續(xù)訪問ECS實(shí)例。
端口范圍:SSH(22)、RDP(3389)、HTTP(80)、HTTPS(443)。
說明端口范圍處選中的是ECS實(shí)例上運(yùn)行的應(yīng)用需開放的端口。
此處創(chuàng)建的安全組默認(rèn)設(shè)置0.0.0.0/0作為源的規(guī)則。0.0.0.0/0表示允許全網(wǎng)段設(shè)備訪問指定的端口,如果您知道請(qǐng)求端的IP地址,建議后續(xù)設(shè)置為具體的IP范圍。具體操作,請(qǐng)參見修改安全組規(guī)則。
創(chuàng)建密鑰對(duì)
密鑰對(duì)可作為登錄時(shí)證明個(gè)人身份的安全憑證,創(chuàng)建完成后,必須下載私鑰,以供后續(xù)連接ECS實(shí)例時(shí)使用。創(chuàng)建完畢后返回ECS購買頁,刷新并選擇密鑰對(duì)。
root
具有操作系統(tǒng)的最高權(quán)限,使用root
作為登錄名可能會(huì)導(dǎo)致安全風(fēng)險(xiǎn),建議您選擇ecs-user
作為登錄名。說明創(chuàng)建密鑰對(duì)后,私鑰會(huì)自動(dòng)下載,請(qǐng)您關(guān)注瀏覽器的下載記錄,保存
.pem
格式的私鑰文件。
創(chuàng)建并查看ECS實(shí)例
創(chuàng)建或選擇好ECS實(shí)例所需的基礎(chǔ)資源后,勾選《云服務(wù)器ECS服務(wù)條款》、《云服務(wù)器ECS退訂說明》,單擊確認(rèn)下單。在提示成功的對(duì)話框中,單擊管理控制臺(tái),即可在控制臺(tái)查看到創(chuàng)建好的ECS實(shí)例。請(qǐng)您保存以下數(shù)據(jù),以便在后續(xù)操作中使用。
實(shí)例ID:便于在實(shí)例列表中查詢到該實(shí)例。
地域:便于在實(shí)例列表中查詢到該實(shí)例。
公網(wǎng)IP地址:便于在后續(xù)使用ECS實(shí)例時(shí),做Web服務(wù)的部署結(jié)果驗(yàn)證。
操作二:連接ECS實(shí)例
在云服務(wù)器ECS控制臺(tái)的實(shí)例列表頁面,根據(jù)地域、實(shí)例ID找到創(chuàng)建好的ECS實(shí)例,單擊操作列下的遠(yuǎn)程連接。
在遠(yuǎn)程連接對(duì)話框中,單擊通過Workbench遠(yuǎn)程連接對(duì)應(yīng)的立即登錄。
在登錄實(shí)例對(duì)話框中,選擇認(rèn)證方式為SSH密鑰認(rèn)證,用戶名為
root
,輸入或上傳創(chuàng)建密鑰對(duì)時(shí)下載的私鑰文件,單擊確定,即可登錄ECS實(shí)例。說明私鑰文件在創(chuàng)建密鑰對(duì)時(shí)自動(dòng)下載到本地,請(qǐng)您關(guān)注瀏覽器的下載記錄,查找
.pem
格式的私鑰文件。顯示如下頁面后,即說明您已成功登錄ECS實(shí)例。
操作三:在訪問控制中創(chuàng)建RAM角色
進(jìn)入RAM訪問控制的創(chuàng)建角色頁面。
在創(chuàng)建角色頁面勾選阿里云服務(wù),單擊下一步。
勾選普通服務(wù)角色,填寫角色名稱如
oss-web-upload
。并選擇受信服務(wù)為云服務(wù)器后,單擊完成,即可完成創(chuàng)建。單擊復(fù)制,保存角色的ARN。
操作四:在訪問控制中創(chuàng)建上傳文件的權(quán)限策略
在權(quán)限策略頁面,單擊創(chuàng)建權(quán)限策略。
在創(chuàng)建權(quán)限策略頁面,單擊腳本編輯,將以下腳本中的
<Bucket名稱>
替換為準(zhǔn)備工作中創(chuàng)建的Bucket名稱web-direct-upload
。{ "Version": "1", "Statement": [ { "Effect": "Allow", "Action": "oss:PutObject", "Resource": "acs:oss:*:*:<Bucket名稱>/*" } ] }
然后單擊確定,填寫策略名稱。
操作五:在訪問控制為RAM角色授予權(quán)限
在角色頁面,找到目標(biāo)RAM角色,然后單擊RAM角色右側(cè)的新增授權(quán)。
在新增授權(quán)頁面下的自定義策略頁簽,選擇已創(chuàng)建的自定義權(quán)限策略。
單擊確定。
操作六:為ECS綁定RAM角色
進(jìn)入云服務(wù)器ECS的實(shí)例頁面,在頁面上方選擇ECS實(shí)例所處地域,然后在實(shí)例頁面單擊目標(biāo)實(shí)例右側(cè)按鈕,單擊授予/收回RAM角色。
在授予/收回RAM角色彈出框選擇目標(biāo)RAM角色,完成ECS綁定RAM角色。
生成的臨時(shí)身份憑證用于下一步生成簽名URL,如果您已經(jīng)擁有臨時(shí)身份憑證,可以直接跳轉(zhuǎn)到下一步三、服務(wù)端生成URL簽名。
二、服務(wù)端生成臨時(shí)訪問憑證
操作一、ECS服務(wù)端配置依賴
請(qǐng)執(zhí)行以下命令安裝獲取臨時(shí)訪問憑證所需要的依賴項(xiàng)。
Python
安裝Python3。
執(zhí)行以下命令,安裝Credentials工具。
sudo pip install oss2
sudo pip install alibabacloud_credentials
Java
在Maven項(xiàng)目中,導(dǎo)入以下依賴。
<!-- https://mvnrepository.com/artifact/com.aliyun/credentials-java -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>credentials-java</artifactId>
<version>0.3.4</version>
</dependency>
<dependency>
<groupId>com.aliyun.kms</groupId>
<artifactId>kms-transfer-client</artifactId>
<version>0.1.0</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
操作二、在ECS服務(wù)端獲取臨時(shí)身份憑證
通過在業(yè)務(wù)服務(wù)器集成STS SDK,實(shí)現(xiàn)獲取臨時(shí)STS身份憑證,并將其返回給請(qǐng)求者。
Python
from alibabacloud_credentials.client import Client as CredClient
from alibabacloud_credentials.models import Config as CredConfig
def main():
# 配置ECSRAMRole作為訪問憑證。
credentialConfig = CredConfig(
type='ecs_ram_role',
role_name='ecs_role_name' # 請(qǐng)?zhí)顚慐CS扮演的角色名稱。
)
credentialsClient = CredClient(credentialConfig)
credential = credentialsClient.get_credential()
accesskeyid = credential.access_key_id # 獲取accesskeyid。
accesskeysecret = credential.access_key_secret # 獲取accesskeysecret。
security_token = credential.security_token # 獲取security_token。
print("stsToken:", security_token)
print("accesskeyid:", accesskeyid)
print("accesskeysecret:", accesskeysecret)
if __name__ == "__main__":
main()
Java
import com.aliyun.credentials.models.CredentialModel;
import com.aliyun.oss.common.auth.Credentials;
import com.aliyun.oss.common.auth.CredentialsProvider;
import com.aliyun.oss.common.auth.DefaultCredentials;
import com.aliyun.oss.common.utils.BinaryUtil;
public class vxDemo {
public static void main(String[] args) {
// 配置ECSRAMRole作為訪問憑證。
com.aliyun.credentials.models.Config config = new com.aliyun.credentials.models.Config();
config.setType("ecs_ram_role");
config.setRoleName("ecs_role_name"); // 請(qǐng)?zhí)顚慐CS扮演的角色名稱。
final com.aliyun.credentials.Client credentialsClient = new com.aliyun.credentials.Client(config);
CredentialsProvider credentialsProvider = new CredentialsProvider() {
@Override
public void setCredentials(Credentials credentials) {
}
@Override
public Credentials getCredentials() {
CredentialModel credential = credentialsClient.getCredential();
return new DefaultCredentials(credential.getAccessKeyId(), credential.getAccessKeySecret(), credential.getSecurityToken());
}
};
String accessKeyId = credentialsProvider.getCredentials().getAccessKeyId(); //獲取accessKeyId。
String secretAccessKey = credentialsProvider.getCredentials().getSecretAccessKey(); //獲取secretAccessKey。
String securityToken = credentialsProvider.getCredentials().getSecurityToken(); //獲取securityToken。
// 打印臨時(shí)訪問憑證信息。
System.out.println("stsToken:" + securityToken);
System.out.println("accessKeyId:" + accessKeyId);
System.out.println("accesskeySecret:"+ secretAccessKey);
}
}
生成的臨時(shí)訪問憑證accessKeyId、accessKeySecret、stsToken用于在下一步中填入生成簽名URL。
三、服務(wù)端生成URL簽名
客戶端向服務(wù)端發(fā)送一個(gè) POST 請(qǐng)求,包含文件名、HTTP 方法、請(qǐng)求頭、查詢參數(shù)等。服務(wù)端接收到請(qǐng)求后,通過OSS Client生成一個(gè)簽名URL,該URL允許用戶上傳文件到OSS。
app.post('/get_sign_url', async (req, res) => {
const {
fileName,
method,
headers = {},
queries = {},
additionalHeaders = []
} = req.body; // 從body中解析出數(shù)據(jù)
const client = new OSS({
region: 'yourRegion',
// 填入步驟二中返回的臨時(shí)訪問憑證accessKeyId、accessKeySecret、stsToken
accessKeyId: 'yourstsAccessKey',
accessKeySecret: 'yourstsAccessKeySecret',
stsToken: 'yourSTSToken',
bucket: 'yourBucket',
authorizationV4: true
});
const reqHeaders = {
...headers
};
// 處理一下content-type
if (fileName && method === 'PUT') {
const fileNameSplit = fileName.split('.');
reqHeaders['content-type'] = mime.getType(fileNameSplit.length > 1 ? fileNameSplit[fileNameSplit.length-1] : '');
}
// 生成V4簽名URL
const url = await client.signatureUrlV4(method, 300, {
headers: reqHeaders,
queries
}, fileName, additionalHeaders);
res.json({
url,
contentType: reqHeaders['content-type']
});
});
步驟三:配置鴻蒙客戶端
一、構(gòu)建HTTP數(shù)據(jù)請(qǐng)求
使用 @kit.NetworkKit
庫的 http
模塊來發(fā)送 HTTP 請(qǐng)求的異步函數(shù) request
。
import { http } from '@kit.NetworkKit';
const request = async (url: string, options: http.HttpRequestOptions, successCode: number[] | number) => {
const httpRequest = http.createHttp();
try {
const httpResponse = await httpRequest.request(url, {
...options,
priority: 1,
connectTimeout: 60000,
readTimeout: 60000,
usingProtocol: http.HttpProtocol.HTTP1_1
});
if ((Array.isArray(successCode) && successCode.includes(httpResponse.responseCode)) || httpResponse.responseCode === successCode) {
const requestID = httpResponse.header['x-oss-request-id'];
console.info(`request success${requestID ? ', oss request ID: ' + requestID : ''}`);
return httpResponse;
} else {
throw {
code: httpResponse.responseCode,
result: httpResponse.result.toString(),
requestID: httpResponse.header['x-oss-request-id']
};
}
} catch (err) {
console.info('request error: ' + JSON.stringify(err));
throw err;
} finally {
httpRequest.destroy();
}
};
export {
request
};
二、從本地獲取要上傳的文件
import { common } from '@kit.AbilityKit';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import picker from '@ohos.file.picker';
// 選擇文件
const fileSelect = async (context: common.Context) => {
const documentSelectOptions = new picker.DocumentSelectOptions();
const documentViewPicker = new picker.DocumentViewPicker(context);
documentSelectOptions.maxSelectNumber = 5;
const documentSelectResult = await documentViewPicker.select(documentSelectOptions);
return documentSelectResult;
};
export {
fileSelect
};
三、客戶端發(fā)送請(qǐng)求生成簽名URL
客戶端發(fā)起請(qǐng)求到服務(wù)器請(qǐng)求生成簽名 URL。請(qǐng)求體包含了文件名、請(qǐng)求方法、頭部信息等。成功響應(yīng)后,返回簽名 URL 和 Content-Type
。如果請(qǐng)求失敗,捕獲并拋出錯(cuò)誤。
const getSignUrl = async (fileName: string, req: {
method: 'GET' | 'POST' | 'PUT';
headers?: Record<string, string | number>;
queries?: Record<string, string>;
additionalHeaders?: string[];
}): Promise<ISignUrlResult> => {
console.info('in getSignUrl');
try {
const response = await request(serverUrl, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json'
},
extraData: {
fileName,
method: req.method,
headers: req.headers,
queries: req.queries,
additionalHeaders: req.additionalHeaders
},
expectDataType: http.HttpDataType.OBJECT
}, 200);
const result = response.result as ISignUrlResult;
console.info('success getSignUrl');
return result;
} catch (err) {
console.info('getSignUrl request error: ' + JSON.stringify(err));
throw err;
}
};
四、上傳文件
獲取在服務(wù)端生成的簽名URL,然后使用PutObject的方法上傳文件。
const putObject = async (fileUri: string): Promise<void> => {
console.info('in putObject');
const fileInfo = await fs.open(fileUri, fs.OpenMode.READ_ONLY);
const fileStat = await fs.stat(fileInfo.fd);
let signUrlResult: ISignUrlResult;
console.info('file name: ', fileInfo.name);
try {
// 獲取PutObject的簽名URL
signUrlResult = await getSignUrl(fileInfo.name, {
method: 'PUT',
headers: {
'Content-Length': fileStat.size
},
additionalHeaders: ['Content-Length']
});
} catch (e) {
await fs.close(fileInfo.fd);
throw e;
}
const data = new ArrayBuffer(fileStat.size);
await fs.read(fileInfo.fd, data);
await fs.close(fileInfo.fd);
try {
// 使用PutObject方法上傳文件
await request(signUrlResult.url, {
method: http.RequestMethod.PUT,
header: {
'Content-Length': fileStat.size,
'Content-Type': signUrlResult.contentType
},
extraData: data
}, 200);
console.info('success putObject');
} catch (err) {
console.info('putObject request error: ' + JSON.stringify(err));
throw err;
}
};
結(jié)果驗(yàn)證
以上步驟部署完成后,您可以鴻蒙環(huán)境中實(shí)現(xiàn)上傳文件到OSS的功能,效果示例如下:
點(diǎn)擊Upload File按鈕選擇上傳的文件。
在Bucket列表頁面,選擇您之前創(chuàng)建的用來存放用戶上傳文件的Bucket并打開。您可以在上傳列表看到用戶在鴻蒙端上傳的文件。
清理資源
在本方案中,您創(chuàng)建了1臺(tái)ECS實(shí)例、1個(gè)OSS Bucket、和1個(gè)RAM角色。測試完方案后,您可以參考以下規(guī)則處理對(duì)應(yīng)產(chǎn)品的資源,避免繼續(xù)產(chǎn)生費(fèi)用或產(chǎn)生安全風(fēng)險(xiǎn)。
釋放ECS實(shí)例
如果您不再需要這臺(tái)實(shí)例,可以將其釋放。釋放后,實(shí)例停止計(jì)費(fèi),數(shù)據(jù)不可恢復(fù)。具體操作如下:
返回云服務(wù)器ECS控制臺(tái)的實(shí)例列表頁面,根據(jù)地域、實(shí)例ID找到目標(biāo)ECS實(shí)例,單擊操作列下的。
選擇釋放。
確認(rèn)實(shí)例無誤后,選擇立即釋放,單擊下一步。
確認(rèn)即將釋放的關(guān)聯(lián)資源,并了解相關(guān)數(shù)據(jù)風(fēng)險(xiǎn)后,單擊確認(rèn),即可完成ECS實(shí)例的釋放。
系統(tǒng)盤、分配的公網(wǎng)IP將隨實(shí)例釋放。
安全組、交換機(jī)和VPC不會(huì)隨實(shí)例釋放,但它們均為免費(fèi)資源,您可根據(jù)實(shí)際的業(yè)務(wù)需要選擇性刪除。
彈性公網(wǎng)IP不會(huì)隨實(shí)例釋放,且不是免費(fèi)資源,您可根據(jù)實(shí)際的業(yè)務(wù)需要選擇性刪除。
刪除Bucket
單擊Bucket 列表,然后單擊目標(biāo)Bucket名稱。
刪除Bucket的所有文件(Object)。
在左側(cè)導(dǎo)航欄,單擊刪除Bucket,然后按照頁面指引完成刪除操作。
刪除RAM角色
使用RAM管理員登錄RAM控制臺(tái)。
在左側(cè)導(dǎo)航欄,選擇 。
在角色頁面,單擊目標(biāo)RAM角色操作列的刪除角色。
在刪除角色對(duì)話框,輸入RAM角色名稱,然后點(diǎn)擊刪除角色。
說明如果RAM角色被授予了權(quán)限策略,刪除角色時(shí),會(huì)同時(shí)解除授權(quán)。
常見問題
如何實(shí)現(xiàn)分片上傳?
當(dāng)您希望使用簽名URL以分片上傳的方式上傳大文件到OSS時(shí),您需要先初始化分片上傳,然后為每一個(gè)分片生成一個(gè)對(duì)應(yīng)的上傳簽名URL,并返回給客戶端??蛻舳丝梢允褂眠@些簽名URL上傳所有的分片信息,然后合并分片來達(dá)到通過簽名URL實(shí)現(xiàn)分片上傳的目的。具體代碼實(shí)現(xiàn)可參考:
import { http } from '@kit.NetworkKit';
import fs from '@ohos.file.fs';
import { getSignUrl } from './upload';
import { request } from './request';
import { xmlToObj } from './xml';
type TPart = {
partNum: number;
etag: string;
};
type TTodoPart = {
partLength: number;
partNum: number;
}
/**
* InitiateMultipartUpload
* @param fileName 文件名
*/
const initiateMultipartUpload = async (fileName: string) => {
console.info('in initiateMultipartUpload');
// 獲取InitiateMultipartUpload簽名URL
const signUrlResult = await getSignUrl(fileName, {
method: 'POST',
queries: {
uploads: null
}
});
try {
// 通過InitiateMultipartUpload接口來通知OSS初始化一個(gè)Multipart Upload事件
const response = await request(signUrlResult.url, {
method: http.RequestMethod.POST,
expectDataType: http.HttpDataType.STRING
}, 200);
const result = response.result as string;
console.info('success initiateMultipartUpload');
const res = xmlToObj(result) as {
InitiateMultipartUploadResult: {
Bucket: string;
Key: string;
UploadId: string;
EncodingType?: string;
}
};
return res.InitiateMultipartUploadResult;
} catch (err) {
console.info('initiateMultipartUpload request error: ' + JSON.stringify(err));
throw err;
}
};
/**
* UploadPart
* @param uploadId 分片上傳的uploadId
* @param partNum 分片上傳的partNumber
* @param file 上傳的文件
* @param length 分片大小
* @param offset 文件讀取位置
*/
const uploadPart = async (uploadId: string, partNum: number, file: fs.File, length: number, offset: number = 0) => {
console.info('in uploadPart');
// 獲取UploadPart簽名URL
const signUrlResult = await getSignUrl(file.name, {
method: 'PUT',
headers: {
'Content-Length': length
},
queries: {
uploadId,
partNumber: partNum.toString()
},
additionalHeaders: ['Content-Length']
});
const data = new ArrayBuffer(length);
await fs.read(file.fd, data, {
length,
offset
});
try {
const response = await request(signUrlResult.url, {
method: http.RequestMethod.PUT,
header: {
'Content-Length': length,
'Content-Type': signUrlResult.contentType
},
extraData: data
}, 200);
console.info('success uploadPart');
return response.header['etag'] as string;
} catch (err) {
console.info('uploadPart request error: ' + JSON.stringify(err));
throw err;
}
};
/**
* CompleteMultipartUpload
* @param fileName 文件名
* @param uploadId 分片上傳的uploadId
* @param completeAll 指定是否列舉當(dāng)前UploadId已上傳的所有Part
* @param [parts] CompleteMultipartUpload所需的Part列表
*/
const completeMultipartUpload = async (fileName: string, uploadId: string, completeAll: boolean = false, parts?: TPart[]) => {
console.info('in completeMultipartUpload');
if (!completeAll && !parts) {
throw new Error('completeMultipartUpload needs to pass in parameter parts.');
}
const signUrlResult = await getSignUrl(fileName, {
method: 'POST',
headers: completeAll ? {
'x-oss-complete-all': 'yes'
} : {
'Content-Type': 'application/xml'
},
queries: {
uploadId
}
});
let xml: string;
if (!completeAll) {
const completeParts = parts.concat().sort((a, b) => a.partNum - b.partNum)
.filter((item, index, arr) => !index || item.partNum !== arr[index - 1].partNum);
xml = '<?xml version="1.0" encoding="UTF-8"?>\n<CompleteMultipartUpload>\n';
completeParts.forEach(item => {
xml += `<Part>\n<PartNumber>${item.partNum}</PartNumber>\n<ETag>${item.etag}</ETag>\n</Part>\n`
});
xml += '</CompleteMultipartUpload>';
}
try {
const result = await request(signUrlResult.url, {
method: http.RequestMethod.POST,
header: completeAll ? {
'x-oss-complete-all': 'yes'
} : {
'Content-Type': 'application/xml'
},
extraData: !completeAll ? xml : undefined
}, 200);
console.info('success completeMultipartUpload');
return result;
} catch (err) {
console.info('completeMultipartUpload request error: ' + JSON.stringify(err));
throw err;
}
};
/**
* 分片上傳信息
*/
interface ICheckpoint {
/** 分片上傳的uploadId */
uploadId: string;
/** 文件URI */
fileUri: string;
/** 分片大小 */
partSize: number;
/** 已經(jīng)上傳完成的分片 */
doneParts: TPart[];
}
/**
* 分片上傳
*/
export class MultipartUpload {
/** 分片上傳信息 */
private checkpoint: ICheckpoint;
/** 上傳文件 */
private file: fs.File;
/** 文件詳細(xì)屬性信息 */
private fileStat: fs.Stat;
/** 取消上傳標(biāo)識(shí) */
private cancelFlag = true;
/** 并發(fā)上傳數(shù) */
private parallel = 5;
/** 上傳隊(duì)列 */
private uploadQueue: TTodoPart[] = [];
/** 當(dāng)前正在上傳數(shù) */
private uploadingCount = 0;
/** 上傳失敗分片信息 */
private uploadErrors: {
partNum: number;
uploadError: Error;
}[] = [];
/**
* 創(chuàng)建MultipartUpload實(shí)例
* @param [fileUri] 文件URI
* @param [checkpoint] 分片上傳信息
*/
constructor(fileUri?: string, checkpoint?: ICheckpoint) {
if (checkpoint) {
this.checkpoint = checkpoint;
this.file = fs.openSync(checkpoint.fileUri, fs.OpenMode.READ_ONLY);
} else {
if (!fileUri) {
throw Error('MultipartUpload need fileUri or checkpoint.');
}
this.file = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
this.checkpoint = {
uploadId: '',
fileUri,
partSize: 2 ** 20,
doneParts: []
};
}
this.fileStat = fs.statSync(this.file.fd);
}
private async uploadPart(part: TTodoPart, resolve: () => void) {
this.uploadingCount++;
const {
partLength,
partNum
} = part;
try {
const result = await uploadPart(this.checkpoint.uploadId, partNum, this.file, partLength, (partNum - 1) * this.checkpoint.partSize);
this.checkpoint.doneParts.push({
partNum: partNum,
etag: result
});
this.uploadingCount--;
if(this.uploadErrors.length < 1) {
if (this.uploadQueue.length < 1 && this.uploadingCount < 1) {
resolve();
} else {
this.next(resolve);
}
}
} catch (e) {
this.uploadingCount--;
this.uploadErrors.push({
partNum: partNum,
uploadError: e
});
resolve();
}
}
private next(resolve: () => void) {
if (this.cancelFlag) {
resolve();
}
if (this.uploadQueue.length > 0 && this.uploadingCount < this.parallel && this.uploadErrors.length < 1) {
this.uploadPart(this.uploadQueue.shift(), resolve);
}
}
/**
* 執(zhí)行分片上傳
*/
async multipartUpload() {
this.cancelFlag = false;
this.uploadQueue = [];
this.uploadErrors = [];
if (this.checkpoint.uploadId === '') {
const initResult = await initiateMultipartUpload(this.file.name);
this.checkpoint.uploadId = initResult.UploadId;
}
const partsSum = Math.ceil(this.fileStat.size / this.checkpoint.partSize);
for (let i = 0; i < partsSum; i++) {
if (this.checkpoint.doneParts.findIndex(v => v.partNum === i + 1) === -1) {
this.uploadQueue.push({
partLength: i + 1 === partsSum ? this.fileStat.size % this.checkpoint.partSize : this.checkpoint.partSize,
partNum: i + 1
});
}
}
const tempCount = Math.min(this.parallel, this.uploadQueue.length);
await new Promise<void>((resolve) => {
for (let i = 0; i < tempCount; i++) {
this.next(resolve);
}
});
if (this.cancelFlag) {
throw new Error('MultipartUpload cancel');
}
if (this.uploadErrors.length) {
throw new Error('Upload failed parts: ' + this.uploadErrors.map(i => i.partNum).join(','));
}
return await completeMultipartUpload(this.file.name, this.checkpoint.uploadId, false, this.checkpoint.doneParts);
}
cancel() {
this.cancelFlag = true;
}
};
export {
initiateMultipartUpload,
uploadPart,
completeMultipartUpload
};