fufu-cloud-cli 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fufu_cloud_cli-0.7.0/src/fufu_cloud_cli.egg-info → fufu_cloud_cli-0.9.0}/PKG-INFO +104 -9
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/README.md +98 -8
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/pyproject.toml +6 -3
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/__init__.py +1 -1
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/api_backend.py +33 -14
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/capabilities.json +80 -17
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/capacity.py +43 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/cli.py +75 -16
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/cloud_data.py +5 -4
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/cloudflare.py +331 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/control.py +25 -11
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/docker_backend.py +91 -24
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/http_runtime.py +160 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/iac_state.py +61 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/limits.py +72 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/manifest.py +26 -7
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/object_transfer.py +77 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/pubsub.py +66 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/pulumi-endpoints.json +526 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/pulumi_bridge.py +205 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/remote_compute.py +53 -14
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/runtime/ingress/proxy.py +3 -3
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/runtime_sdk.py +191 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/secrets.py +90 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/serverless.py +8 -6
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/__init__.py +1 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/aws.py +187 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/aws_data.py +63 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/common.py +129 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/google.py +155 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/google_pubsub.py +57 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/remote_state.py +74 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli/terraform_bridge/runner.py +402 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0/src/fufu_cloud_cli.egg-info}/PKG-INFO +104 -9
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/SOURCES.txt +19 -1
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/entry_points.txt +1 -0
- fufu_cloud_cli-0.9.0/src/fufu_cloud_cli.egg-info/requires.txt +13 -0
- fufu_cloud_cli-0.7.0/src/fufu_cloud_cli/runtime_sdk.py +0 -114
- fufu_cloud_cli-0.7.0/src/fufu_cloud_cli.egg-info/requires.txt +0 -4
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/LICENSE +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/MANIFEST.in +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/setup.cfg +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/__main__.py +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/errors.py +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/lambda_adapter.py +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/public_access.py +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/runtime/functions/Dockerfile +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli/runtime/ingress/Dockerfile +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/dependency_links.txt +0 -0
- {fufu_cloud_cli-0.7.0 → fufu_cloud_cli-0.9.0}/src/fufu_cloud_cli.egg-info/top_level.txt +0 -0
|
@@ -1,22 +1,67 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fufu-cloud-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: FUFU Cloud CLI for tenant-scoped mock cloud development and deployment
|
|
5
5
|
License-Expression: LicenseRef-Proprietary
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: PyYAML==6.0.3
|
|
10
|
+
Requires-Dist: tomli==2.2.1; python_version < "3.11"
|
|
10
11
|
Provides-Extra: yaml
|
|
11
12
|
Requires-Dist: PyYAML==6.0.3; extra == "yaml"
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: boto3==1.40.66; extra == "test"
|
|
15
|
+
Provides-Extra: terraform
|
|
16
|
+
Requires-Dist: python-hcl2==7.3.1; extra == "terraform"
|
|
12
17
|
Dynamic: license-file
|
|
13
18
|
|
|
14
19
|
# FUFU Cloud CLI
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
`gcloud-fufu`、`az-fufu`。這是 AWS、Google Cloud 與
|
|
21
|
+
以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
|
|
22
|
+
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
18
23
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
19
24
|
|
|
25
|
+
0.8.0 包含資源配額、分段傳輸、Terraform 核心子集與 Cloudflare 靜態部署/R2。需要同步更新服務端;CLI 安裝成功不表示任意 FUFU 服務端都已有這些能力。
|
|
26
|
+
|
|
27
|
+
## Cloudflare 前端部署與 R2
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install 'fufu-cloud-cli==0.8.0'
|
|
31
|
+
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
32
|
+
wrangler-fufu capabilities
|
|
33
|
+
wrangler-fufu pages project create frontend --production-branch main
|
|
34
|
+
npm run build
|
|
35
|
+
wrangler-fufu pages deploy dist --project-name frontend --branch main
|
|
36
|
+
wrangler-fufu pages deployment list --project-name frontend
|
|
37
|
+
wrangler-fufu deploy --assets dist --name frontend-assets
|
|
38
|
+
wrangler-fufu r2 bucket create frontend-files
|
|
39
|
+
wrangler-fufu r2 object put frontend-files/example.txt --file example.txt --remote
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
支援 Wrangler JSON、JSONC、TOML,以及 `--config`、`--cwd`、`--env`。Pages 使用
|
|
43
|
+
`pages_build_output_dir`;Workers Static Assets 使用 `assets.directory`,SPA 可明確設定
|
|
44
|
+
`assets.not_found_handling = "single-page-application"`。正式與預覽網址分開,正式部署完整
|
|
45
|
+
上傳且校驗通過才切换入口;Pages 可用 `pages deployment rollback ID --project-name NAME` 回滾。
|
|
46
|
+
|
|
47
|
+
這些命令提供真正的靜態 HTTP hosting,**不執行 Workers JavaScript 或 Pages Functions**。
|
|
48
|
+
KV、D1、Durable Objects、Queues、Cron、自訂網域、SSR 與全球 CDN 快取未實作;
|
|
49
|
+
相關配置會被拒絕。API credential、S3 endpoint 與帳號識別使用 FUFU 值。
|
|
50
|
+
目前不讀取官方 `CLOUDFLARE_API_TOKEN`;使用 FUFU credential JSON,避免誤用正式金鑰。
|
|
51
|
+
|
|
52
|
+
R2 支援 bucket/object、分段上傳、範圍下載、CORS、預設私人及可開關公開開發網址。
|
|
53
|
+
原生 boto3/AWS S3 SDK 可使用 credential JSON 中的 `r2_endpoint`、`r2_access_key_id`、
|
|
54
|
+
`r2_secret_access_key`,region 為 `auto` 且使用 path-style addressing。
|
|
55
|
+
SDK checksum 配置使用 `WHEN_REQUIRED`;SigV4、預簽 PUT/GET、Content-MD5、
|
|
56
|
+
基本自訂 metadata 與 multipart 已支援。未支援條件寫入、CopyObject、版本/生命週期、
|
|
57
|
+
自訂加密,以及 Cache-Control/Content-Encoding 等寫入 metadata;會明確回錯,不靜默忽略。
|
|
58
|
+
沒有 Cloudflare Terraform provider 相容層。
|
|
59
|
+
|
|
60
|
+
静態檔案上限 25 MiB/每次最多 100,000 檔;R2 物件上限 5 TiB、S3 單次或分段上限
|
|
61
|
+
5 GiB、最多 10,000 段。可用磁碟、請求逾時與併發容量仍會限制實際可接受大小。
|
|
62
|
+
公開部署前拒絕私人路徑、符號連結與已知 credential 格式;此檢查不是所有機密的偵測器。
|
|
63
|
+
FUFU 沒有 CDN 快取,入口回覆 `Cache-Control: no-store`,不模擬一次成功的 cache purge。
|
|
64
|
+
|
|
20
65
|
## 安裝
|
|
21
66
|
|
|
22
67
|
需要 Python 3.10 以上。Python 套件預設包含 YAML 相依套件:
|
|
@@ -26,8 +71,8 @@ pip install fufu-cloud-cli
|
|
|
26
71
|
fufu --version
|
|
27
72
|
```
|
|
28
73
|
|
|
29
|
-
npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10
|
|
30
|
-
呼叫相同的 Python CLI。npm
|
|
74
|
+
npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
|
|
75
|
+
呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
|
|
31
76
|
不會自行下載 Python 或執行 pip。
|
|
32
77
|
|
|
33
78
|
```bash
|
|
@@ -43,6 +88,27 @@ npx --no-install aws-fufu --help
|
|
|
43
88
|
|
|
44
89
|
## 憑證與 CI
|
|
45
90
|
|
|
91
|
+
目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
|
|
92
|
+
Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
|
|
93
|
+
復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
|
|
94
|
+
已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
|
|
95
|
+
DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
|
|
96
|
+
的 0.8.0,不能把套件安裝成功當成支援驗收。
|
|
97
|
+
|
|
98
|
+
此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
|
|
99
|
+
管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
|
|
100
|
+
FUFU state。Terraform 需另行安裝,Python 使用 `fufu-cloud-cli[terraform]`
|
|
101
|
+
extra;npm 使用的 Python 也需要該 extra 的相依套件。使用 0.8.0 以上版本。
|
|
102
|
+
GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
|
|
103
|
+
不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
|
|
104
|
+
與獨立 state,不可直接搬用 FUFU state。
|
|
105
|
+
|
|
106
|
+
後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
|
|
107
|
+
subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
|
|
108
|
+
已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
|
|
109
|
+
查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
|
|
110
|
+
exactly-once、Function trigger 或保留期限保證。
|
|
111
|
+
|
|
46
112
|
向 FUFU Cloud 管理者取得該租戶、該 provider 的專用 credential JSON。
|
|
47
113
|
CLI 不附預設帳號、共用金鑰或可用的租戶憑證,也不讀取 `.env`、瀏覽器
|
|
48
114
|
Cookie 或正式雲端登入工作階段。
|
|
@@ -54,11 +120,13 @@ export FUFU_BACKEND=api
|
|
|
54
120
|
export FUFU_AWS_CREDENTIAL_FILE=/run/secrets/fufu-aws.json
|
|
55
121
|
export FUFU_GCP_CREDENTIAL_FILE=/run/secrets/fufu-gcp.json
|
|
56
122
|
export FUFU_AZURE_CREDENTIAL_FILE=/run/secrets/fufu-azure.json
|
|
123
|
+
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
57
124
|
|
|
58
125
|
fufu auth whoami --provider aws
|
|
59
126
|
aws-fufu sts get-caller-identity
|
|
60
127
|
gcloud-fufu auth list
|
|
61
128
|
az-fufu account show
|
|
129
|
+
wrangler-fufu whoami
|
|
62
130
|
```
|
|
63
131
|
|
|
64
132
|
credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅限
|
|
@@ -74,6 +142,32 @@ Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header si
|
|
|
74
142
|
|
|
75
143
|
## 部署與支援範圍
|
|
76
144
|
|
|
145
|
+
0.9.0 的配套後端新增 AWS/GCP Secrets Manager、GCP 部署秘密參照、
|
|
146
|
+
Cloud Run SSE/WebSocket、HTTP Functions SSE,以及目前運行版本的身分續期。
|
|
147
|
+
服務端須配置私有加密金鑰;未配置時會明確拒絕秘密操作。
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
aws-fufu secretsmanager create-secret --name app-key --secret-string file://./dev-secret.txt
|
|
151
|
+
aws-fufu secretsmanager get-secret-value --secret-id app-key --out-file ./private-result.txt
|
|
152
|
+
gcloud-fufu secrets create app-key --data-file ./dev-secret.txt
|
|
153
|
+
gcloud-fufu run deploy example --source ./prepared-asgi.zip \
|
|
154
|
+
--service-account "$WORKLOAD_SERVICE_ACCOUNT" --set-secrets APP_KEY=app-key:1
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
秘密值只由受控檔案或 `--secret-env VARIABLE` 輸入;讀取需指定私有新檔案,
|
|
158
|
+
不會輸出到終端。Cloud Run/Functions 的 service account 需 Secret Accessor
|
|
159
|
+
權限。支援 `--update-secrets`、`--clear-secrets` 和 Knative `secretKeyRef`。
|
|
160
|
+
這是本機加密版本管理子集:AWS 為 UTF-8 SecretString,單值最大 64 KiB;
|
|
161
|
+
無 KMS、rotation、跨區域複寫、排程刪除與復原。立即刪除或銷毀後,加密
|
|
162
|
+
備份與既有容器可能仍持有舊值。CLI metadata 採 FUFU 共用形狀,並非完整
|
|
163
|
+
官方 JSON;GCP runtime 使用明確 helper,無全量 Google SDK/ADC 替換。
|
|
164
|
+
新的秘密資源與 Terraform/OpenTofu/Pulumi 參照尚未接通。
|
|
165
|
+
|
|
166
|
+
Cloud Run 串流回應不設原本的 32 MiB 緩衝上限,仍受服務期限及容量限制。
|
|
167
|
+
HTTP Functions Gen 1/Gen 2 支援 SSE、無 WebSocket;Gen 2 保留較寬鬆的
|
|
168
|
+
32 MiB response,而官方 streaming response 是 10 MB。Lambda 仍為 buffered。
|
|
169
|
+
公網 DNS、TLS、WAF 及 CDN 期限須另外驗證。
|
|
170
|
+
|
|
77
171
|
```bash
|
|
78
172
|
fufu capabilities
|
|
79
173
|
fufu capabilities --remote --provider gcp --require cloud-run-http
|
|
@@ -88,7 +182,7 @@ aws-fufu lambda list-functions
|
|
|
88
182
|
接受已封裝 Uvicorn 與相依套件的 Python 3.12 ASGI ZIP/乾淨目錄,自動離線建置、
|
|
89
183
|
檢查映像並核准到目前租戶;不需要主機 SSH 或每次人工核准。
|
|
90
184
|
`--entry-point module:app` 是 FUFU 擴充旗標,預設 `main:app`。
|
|
91
|
-
|
|
185
|
+
目前來源版本的 FUFU 配方上限為 ZIP 512 MiB、解壓縮 2 GiB、100000 個檔案;不接受隱藏檔、Dockerfile、
|
|
92
186
|
自訂安裝設定或非空 requirements.txt。相依套件需在 CI 封裝,服務端不執行安裝 hook。
|
|
93
187
|
這是固定 ASGI 配方,不提供完整 Cloud Build 或 registry API。
|
|
94
188
|
成功回應包含 `build.status`、`imageDigest` 與來源包雜湊;自動核准映像
|
|
@@ -103,8 +197,8 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
103
197
|
| --- | --- |
|
|
104
198
|
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
105
199
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
106
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL
|
|
107
|
-
| GCS / S3 | bucket 與 object
|
|
200
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;ZIP 50 MiB/解壓 250 MiB,同步 payload 6 MiB |
|
|
201
|
+
| GCS / S3 | bucket 與 object 子集;分段上傳/串流下載,受主機磁碟限制;已驗證超過 1 GiB,無 versioning / signed URL |
|
|
108
202
|
| DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
|
|
109
203
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
110
204
|
| API Gateway / Serverless profile | 簡化 HTTP Lambda proxy 和明確來源清單;不執行 plugins / CloudFormation |
|
|
@@ -142,7 +236,8 @@ UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名
|
|
|
142
236
|
`allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
|
|
143
237
|
(`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
|
|
144
238
|
(`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
|
|
145
|
-
入口支援有界限的普通 HTTP
|
|
239
|
+
入口支援有界限的普通 HTTP:Lambda 請求與回應各 6 MiB、Gen 1 各 10 MiB、Run/Gen 2 各 32 MiB。
|
|
240
|
+
依服務設定等待,期限上限分別為 900/540/3600 秒;外部 CDN 期限另計。每服務每分鐘 6000 次,另有主機容量背壓。
|
|
146
241
|
不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
|
|
147
242
|
GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
|
|
148
243
|
`print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
# FUFU Cloud CLI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`gcloud-fufu`、`az-fufu`。這是 AWS、Google Cloud 與
|
|
3
|
+
以五個指令操作 FUFU Cloud 的租戶隔離開發環境:`fufu`、`aws-fufu`、
|
|
4
|
+
`gcloud-fufu`、`az-fufu`、`wrangler-fufu`。這是 AWS、Google Cloud、Azure 與 Cloudflare 的**明確子集**,
|
|
5
5
|
只供 FUFU mock 環境使用;不會把部署轉送到正式雲端。
|
|
6
6
|
|
|
7
|
+
0.8.0 包含資源配額、分段傳輸、Terraform 核心子集與 Cloudflare 靜態部署/R2。需要同步更新服務端;CLI 安裝成功不表示任意 FUFU 服務端都已有這些能力。
|
|
8
|
+
|
|
9
|
+
## Cloudflare 前端部署與 R2
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install 'fufu-cloud-cli==0.8.0'
|
|
13
|
+
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
14
|
+
wrangler-fufu capabilities
|
|
15
|
+
wrangler-fufu pages project create frontend --production-branch main
|
|
16
|
+
npm run build
|
|
17
|
+
wrangler-fufu pages deploy dist --project-name frontend --branch main
|
|
18
|
+
wrangler-fufu pages deployment list --project-name frontend
|
|
19
|
+
wrangler-fufu deploy --assets dist --name frontend-assets
|
|
20
|
+
wrangler-fufu r2 bucket create frontend-files
|
|
21
|
+
wrangler-fufu r2 object put frontend-files/example.txt --file example.txt --remote
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
支援 Wrangler JSON、JSONC、TOML,以及 `--config`、`--cwd`、`--env`。Pages 使用
|
|
25
|
+
`pages_build_output_dir`;Workers Static Assets 使用 `assets.directory`,SPA 可明確設定
|
|
26
|
+
`assets.not_found_handling = "single-page-application"`。正式與預覽網址分開,正式部署完整
|
|
27
|
+
上傳且校驗通過才切换入口;Pages 可用 `pages deployment rollback ID --project-name NAME` 回滾。
|
|
28
|
+
|
|
29
|
+
這些命令提供真正的靜態 HTTP hosting,**不執行 Workers JavaScript 或 Pages Functions**。
|
|
30
|
+
KV、D1、Durable Objects、Queues、Cron、自訂網域、SSR 與全球 CDN 快取未實作;
|
|
31
|
+
相關配置會被拒絕。API credential、S3 endpoint 與帳號識別使用 FUFU 值。
|
|
32
|
+
目前不讀取官方 `CLOUDFLARE_API_TOKEN`;使用 FUFU credential JSON,避免誤用正式金鑰。
|
|
33
|
+
|
|
34
|
+
R2 支援 bucket/object、分段上傳、範圍下載、CORS、預設私人及可開關公開開發網址。
|
|
35
|
+
原生 boto3/AWS S3 SDK 可使用 credential JSON 中的 `r2_endpoint`、`r2_access_key_id`、
|
|
36
|
+
`r2_secret_access_key`,region 為 `auto` 且使用 path-style addressing。
|
|
37
|
+
SDK checksum 配置使用 `WHEN_REQUIRED`;SigV4、預簽 PUT/GET、Content-MD5、
|
|
38
|
+
基本自訂 metadata 與 multipart 已支援。未支援條件寫入、CopyObject、版本/生命週期、
|
|
39
|
+
自訂加密,以及 Cache-Control/Content-Encoding 等寫入 metadata;會明確回錯,不靜默忽略。
|
|
40
|
+
沒有 Cloudflare Terraform provider 相容層。
|
|
41
|
+
|
|
42
|
+
静態檔案上限 25 MiB/每次最多 100,000 檔;R2 物件上限 5 TiB、S3 單次或分段上限
|
|
43
|
+
5 GiB、最多 10,000 段。可用磁碟、請求逾時與併發容量仍會限制實際可接受大小。
|
|
44
|
+
公開部署前拒絕私人路徑、符號連結與已知 credential 格式;此檢查不是所有機密的偵測器。
|
|
45
|
+
FUFU 沒有 CDN 快取,入口回覆 `Cache-Control: no-store`,不模擬一次成功的 cache purge。
|
|
46
|
+
|
|
7
47
|
## 安裝
|
|
8
48
|
|
|
9
49
|
需要 Python 3.10 以上。Python 套件預設包含 YAML 相依套件:
|
|
@@ -13,8 +53,8 @@ pip install fufu-cloud-cli
|
|
|
13
53
|
fufu --version
|
|
14
54
|
```
|
|
15
55
|
|
|
16
|
-
npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10
|
|
17
|
-
呼叫相同的 Python CLI。npm
|
|
56
|
+
npm 版本需要 Node.js 18 以上,以及已安裝的 Python 3.10 以上;各入口
|
|
57
|
+
呼叫相同的 Python CLI。0.8.0 的 npm 建置包內附純 Python YAML/TOML parser,沒有安裝腳本,
|
|
18
58
|
不會自行下載 Python 或執行 pip。
|
|
19
59
|
|
|
20
60
|
```bash
|
|
@@ -30,6 +70,27 @@ npx --no-install aws-fufu --help
|
|
|
30
70
|
|
|
31
71
|
## 憑證與 CI
|
|
32
72
|
|
|
73
|
+
目前工作樹另提供 `fufu tofu`、`fufu pulumi`、`fufu state`。
|
|
74
|
+
Terraform/OpenTofu 可選擇租戶遠端 HTTP state,具備鎖、加密、版本與
|
|
75
|
+
復原;需配套 Console API 和平台金鑰。Pulumi 使用固定官方 Node.js SDK,
|
|
76
|
+
已驗收 SQS、SSM String、Pub/Sub topic/pull subscription,採獨立本機
|
|
77
|
+
DIY backend,秘密欄位需外部 passphrase。這些新能力尚未包含在已發布
|
|
78
|
+
的 0.8.0,不能把套件安裝成功當成支援驗收。
|
|
79
|
+
|
|
80
|
+
此工作樹也提供 `fufu terraform`:以官方 AWS/Google Terraform provider
|
|
81
|
+
管理 Lambda ZIP、Cloud Run v2、IAM 與服務入口的明列子集,並使用獨立的
|
|
82
|
+
FUFU state。Terraform 需另行安裝,Python 使用 `fufu-cloud-cli[terraform]`
|
|
83
|
+
extra;npm 使用的 Python 也需要該 extra 的相依套件。使用 0.8.0 以上版本。
|
|
84
|
+
GCS/S3、Functions Gen 1/Gen 2 等一般 CLI 能力
|
|
85
|
+
不表示官方 Terraform 資源已相容。正式環境使用正常 Terraform、正式認證
|
|
86
|
+
與獨立 state,不可直接搬用 FUFU state。
|
|
87
|
+
|
|
88
|
+
後續開發版另接入 SQS queue、SSM String parameter、Pub/Sub topic/pull
|
|
89
|
+
subscription;新增能力需要配套的 Console 與 Pub/Sub 後端更新,不能從
|
|
90
|
+
已發布的 0.8.0 安裝成功就推定已具備。`gcloud-fufu pubsub` 提供建立、
|
|
91
|
+
查詢、更新、刪除、publish/pull/ack 子集;不支援 push、ordering、
|
|
92
|
+
exactly-once、Function trigger 或保留期限保證。
|
|
93
|
+
|
|
33
94
|
向 FUFU Cloud 管理者取得該租戶、該 provider 的專用 credential JSON。
|
|
34
95
|
CLI 不附預設帳號、共用金鑰或可用的租戶憑證,也不讀取 `.env`、瀏覽器
|
|
35
96
|
Cookie 或正式雲端登入工作階段。
|
|
@@ -41,11 +102,13 @@ export FUFU_BACKEND=api
|
|
|
41
102
|
export FUFU_AWS_CREDENTIAL_FILE=/run/secrets/fufu-aws.json
|
|
42
103
|
export FUFU_GCP_CREDENTIAL_FILE=/run/secrets/fufu-gcp.json
|
|
43
104
|
export FUFU_AZURE_CREDENTIAL_FILE=/run/secrets/fufu-azure.json
|
|
105
|
+
export FUFU_CLOUDFLARE_CREDENTIAL_FILE=/run/secrets/fufu-cloudflare.json
|
|
44
106
|
|
|
45
107
|
fufu auth whoami --provider aws
|
|
46
108
|
aws-fufu sts get-caller-identity
|
|
47
109
|
gcloud-fufu auth list
|
|
48
110
|
az-fufu account show
|
|
111
|
+
wrangler-fufu whoami
|
|
49
112
|
```
|
|
50
113
|
|
|
51
114
|
credential 內含 FUFU endpoint。`FUFU_ENDPOINT` 可明確覆寫目的地:僅限
|
|
@@ -61,6 +124,32 @@ Lambda Function URL 的 AWS_IAM 支援長效 FUFU access key 的 SigV4 header si
|
|
|
61
124
|
|
|
62
125
|
## 部署與支援範圍
|
|
63
126
|
|
|
127
|
+
0.9.0 的配套後端新增 AWS/GCP Secrets Manager、GCP 部署秘密參照、
|
|
128
|
+
Cloud Run SSE/WebSocket、HTTP Functions SSE,以及目前運行版本的身分續期。
|
|
129
|
+
服務端須配置私有加密金鑰;未配置時會明確拒絕秘密操作。
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
aws-fufu secretsmanager create-secret --name app-key --secret-string file://./dev-secret.txt
|
|
133
|
+
aws-fufu secretsmanager get-secret-value --secret-id app-key --out-file ./private-result.txt
|
|
134
|
+
gcloud-fufu secrets create app-key --data-file ./dev-secret.txt
|
|
135
|
+
gcloud-fufu run deploy example --source ./prepared-asgi.zip \
|
|
136
|
+
--service-account "$WORKLOAD_SERVICE_ACCOUNT" --set-secrets APP_KEY=app-key:1
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
秘密值只由受控檔案或 `--secret-env VARIABLE` 輸入;讀取需指定私有新檔案,
|
|
140
|
+
不會輸出到終端。Cloud Run/Functions 的 service account 需 Secret Accessor
|
|
141
|
+
權限。支援 `--update-secrets`、`--clear-secrets` 和 Knative `secretKeyRef`。
|
|
142
|
+
這是本機加密版本管理子集:AWS 為 UTF-8 SecretString,單值最大 64 KiB;
|
|
143
|
+
無 KMS、rotation、跨區域複寫、排程刪除與復原。立即刪除或銷毀後,加密
|
|
144
|
+
備份與既有容器可能仍持有舊值。CLI metadata 採 FUFU 共用形狀,並非完整
|
|
145
|
+
官方 JSON;GCP runtime 使用明確 helper,無全量 Google SDK/ADC 替換。
|
|
146
|
+
新的秘密資源與 Terraform/OpenTofu/Pulumi 參照尚未接通。
|
|
147
|
+
|
|
148
|
+
Cloud Run 串流回應不設原本的 32 MiB 緩衝上限,仍受服務期限及容量限制。
|
|
149
|
+
HTTP Functions Gen 1/Gen 2 支援 SSE、無 WebSocket;Gen 2 保留較寬鬆的
|
|
150
|
+
32 MiB response,而官方 streaming response 是 10 MB。Lambda 仍為 buffered。
|
|
151
|
+
公網 DNS、TLS、WAF 及 CDN 期限須另外驗證。
|
|
152
|
+
|
|
64
153
|
```bash
|
|
65
154
|
fufu capabilities
|
|
66
155
|
fufu capabilities --remote --provider gcp --require cloud-run-http
|
|
@@ -75,7 +164,7 @@ aws-fufu lambda list-functions
|
|
|
75
164
|
接受已封裝 Uvicorn 與相依套件的 Python 3.12 ASGI ZIP/乾淨目錄,自動離線建置、
|
|
76
165
|
檢查映像並核准到目前租戶;不需要主機 SSH 或每次人工核准。
|
|
77
166
|
`--entry-point module:app` 是 FUFU 擴充旗標,預設 `main:app`。
|
|
78
|
-
|
|
167
|
+
目前來源版本的 FUFU 配方上限為 ZIP 512 MiB、解壓縮 2 GiB、100000 個檔案;不接受隱藏檔、Dockerfile、
|
|
79
168
|
自訂安裝設定或非空 requirements.txt。相依套件需在 CI 封裝,服務端不執行安裝 hook。
|
|
80
169
|
這是固定 ASGI 配方,不提供完整 Cloud Build 或 registry API。
|
|
81
170
|
成功回應包含 `build.status`、`imageDigest` 與來源包雜湊;自動核准映像
|
|
@@ -90,8 +179,8 @@ CLI 無法判斷任意原始碼或 ZIP 內的資料是否可以分享。
|
|
|
90
179
|
| --- | --- |
|
|
91
180
|
| Cloud Run | 已核准 HTTP 容器或 API 自動建置 Python ASGI 來源;支援配置後的 HTTPS 入口、Invoker policy;無 autoscaling、traffic splitting、Jobs |
|
|
92
181
|
| Cloud Run functions / Functions Gen 1 | Python 3.12 HTTP 函式;Gen 1 為相同 HTTP runtime 的模擬子集,無背景事件 |
|
|
93
|
-
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL
|
|
94
|
-
| GCS / S3 | bucket 與 object
|
|
182
|
+
| AWS Lambda | Python 3.10 / 3.12 ZIP、同步 invoke、程式碼更新、BUFFERED Function URL;ZIP 50 MiB/解壓 250 MiB,同步 payload 6 MiB |
|
|
183
|
+
| GCS / S3 | bucket 與 object 子集;分段上傳/串流下載,受主機磁碟限制;已驗證超過 1 GiB,無 versioning / signed URL |
|
|
95
184
|
| DynamoDB / SQS / SSM | 明列資料操作;無完整交易、Streams、FIFO 或 KMS / SecureString |
|
|
96
185
|
| AWS IAM / GCP IAM | 租戶內 role / service account / policy 子集;default deny、限定 session;無原生雲端全量 IAM 語義 |
|
|
97
186
|
| API Gateway / Serverless profile | 簡化 HTTP Lambda proxy 和明確來源清單;不執行 plugins / CloudFormation |
|
|
@@ -129,7 +218,8 @@ UI 與 CLI 共用同一份服務政策。Cloud Run 與 HTTP Functions 的匿名
|
|
|
129
218
|
`allUsers` 與對應 Invoker 角色。Lambda `NONE` 必須另加 `lambda:InvokeFunctionUrl`
|
|
130
219
|
(`--function-url-auth-type NONE`)和 `lambda:InvokeFunction`
|
|
131
220
|
(`--invoked-via-function-url`)兩項資源權限;只設定 NONE 不會自動開放呼叫。
|
|
132
|
-
入口支援有界限的普通 HTTP
|
|
221
|
+
入口支援有界限的普通 HTTP:Lambda 請求與回應各 6 MiB、Gen 1 各 10 MiB、Run/Gen 2 各 32 MiB。
|
|
222
|
+
依服務設定等待,期限上限分別為 900/540/3600 秒;外部 CDN 期限另計。每服務每分鐘 6000 次,另有主機容量背壓。
|
|
133
223
|
不支援 SSE、WebSocket、串流、STS 暫時金鑰、presigned URL 或真實 VPC。
|
|
134
224
|
GCP ID token 由 FUFU 的 RS256 issuer 簽發,綁定精確 audience,15 分鐘後過期;不接受 Google ID token。
|
|
135
225
|
`print-identity-token` 明確輸出秘密,應直接放入受控變數,避免 CI 日誌或 shell tracing。
|
|
@@ -4,25 +4,28 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "fufu-cloud-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "FUFU Cloud CLI for tenant-scoped mock cloud development and deployment"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
-
dependencies = ["PyYAML==6.0.3"]
|
|
11
|
+
dependencies = ["PyYAML==6.0.3", "tomli==2.2.1; python_version<'3.11'"]
|
|
12
12
|
license = "LicenseRef-Proprietary"
|
|
13
13
|
license-files = ["LICENSE"]
|
|
14
14
|
|
|
15
15
|
[project.optional-dependencies]
|
|
16
16
|
yaml = ["PyYAML==6.0.3"]
|
|
17
|
+
test = ["boto3==1.40.66"]
|
|
18
|
+
terraform = ["python-hcl2==7.3.1"]
|
|
17
19
|
|
|
18
20
|
[project.scripts]
|
|
19
21
|
fufu = "fufu_cloud_cli.cli:main"
|
|
20
22
|
aws-fufu = "fufu_cloud_cli.cli:aws_main"
|
|
21
23
|
gcloud-fufu = "fufu_cloud_cli.cli:gcloud_main"
|
|
22
24
|
az-fufu = "fufu_cloud_cli.cli:az_main"
|
|
25
|
+
wrangler-fufu = "fufu_cloud_cli.cli:wrangler_main"
|
|
23
26
|
|
|
24
27
|
[tool.setuptools.packages.find]
|
|
25
28
|
where = ["src"]
|
|
26
29
|
|
|
27
30
|
[tool.setuptools.package-data]
|
|
28
|
-
fufu_cloud_cli = ["capabilities.json", "runtime/*/Dockerfile", "runtime/ingress/proxy.py"]
|
|
31
|
+
fufu_cloud_cli = ["capabilities.json", "pulumi-endpoints.json", "runtime/*/Dockerfile", "runtime/ingress/proxy.py"]
|
|
@@ -15,8 +15,10 @@ from urllib.request import HTTPRedirectHandler, HTTPSHandler, ProxyHandler, Requ
|
|
|
15
15
|
|
|
16
16
|
from .errors import FufuError, require
|
|
17
17
|
|
|
18
|
-
PROVIDERS = {"aws", "gcp", "azure"}
|
|
19
|
-
|
|
18
|
+
PROVIDERS = {"aws", "gcp", "azure", "cloudflare"}
|
|
19
|
+
from .limits import JSON_BODY_BYTES, JSON_RESPONSE_BYTES
|
|
20
|
+
|
|
21
|
+
MAX_RESPONSE = 8 * 1024 * 1024
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
@contextmanager
|
|
@@ -71,7 +73,7 @@ def api_selected():
|
|
|
71
73
|
return False
|
|
72
74
|
return mode == "api" or any(os.environ.get(k) for k in (
|
|
73
75
|
"FUFU_ENDPOINT", "FUFU_CREDENTIAL_FILE", "FUFU_AWS_CREDENTIAL_FILE",
|
|
74
|
-
"FUFU_GCP_CREDENTIAL_FILE", "FUFU_AZURE_CREDENTIAL_FILE", 'FUFU_SESSION_FILE')) or os.environ.get("AWS_ACCESS_KEY_ID", "").startswith("FUFU")
|
|
76
|
+
"FUFU_GCP_CREDENTIAL_FILE", "FUFU_AZURE_CREDENTIAL_FILE", "FUFU_CLOUDFLARE_CREDENTIAL_FILE", 'FUFU_SESSION_FILE')) or os.environ.get("AWS_ACCESS_KEY_ID", "").startswith("FUFU")
|
|
75
77
|
|
|
76
78
|
|
|
77
79
|
def load_credential(provider=None, filename=None):
|
|
@@ -93,7 +95,7 @@ def load_credential(provider=None, filename=None):
|
|
|
93
95
|
and data.get("fufu_provider") in PROVIDERS, "INVALID_CREDENTIAL", "需要 FUFU 專用憑證;不接受正式雲端金鑰。")
|
|
94
96
|
actual = data["fufu_provider"]
|
|
95
97
|
require(provider is None or actual == provider, "PROVIDER_MISMATCH", "憑證所屬 provider 不符。")
|
|
96
|
-
fields = {"aws": ("aws_access_key_id", "aws_secret_access_key"), "gcp": ("client_id", "client_secret"), "azure": ("clientId", "clientSecret")}
|
|
98
|
+
fields = {"aws": ("aws_access_key_id", "aws_secret_access_key"), "gcp": ("client_id", "client_secret"), "azure": ("clientId", "clientSecret"), "cloudflare": ("credential_id", "api_token")}
|
|
97
99
|
key, secret = (data.get(k) for k in fields[actual])
|
|
98
100
|
require(isinstance(key, str) and re.fullmatch(r"FUFU" + actual.upper() + r"[A-Za-z0-9_-]{12,100}", key)
|
|
99
101
|
and isinstance(secret, str) and 24 <= len(secret) <= 1024,
|
|
@@ -160,30 +162,48 @@ class ApiClient:
|
|
|
160
162
|
headers["Authorization"] = f"Bearer {self.token}"
|
|
161
163
|
data = None if body is None else json.dumps(body).encode("utf-8")
|
|
162
164
|
if data is not None:
|
|
163
|
-
require(len(data) <= (
|
|
165
|
+
require(len(data) <= (JSON_BODY_BYTES if large else 1024**2), "PAYLOAD_LIMIT", "API JSON payload 超過上限。")
|
|
164
166
|
headers["Content-Type"] = "application/json"
|
|
165
167
|
req = Request(self.endpoint + path, data=data, headers=headers, method=method)
|
|
166
168
|
try:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
duration = min(3630, max(60, int(body.get("timeout", 30)) + 30)) if large and isinstance(body, dict) else 60
|
|
170
|
+
if '/objects/uploads/' in path and path.endswith('/complete'): duration = 3630
|
|
171
|
+
with request_deadline(duration), self.opener.open(req, timeout=duration) as response:
|
|
172
|
+
limit = JSON_RESPONSE_BYTES if large else MAX_RESPONSE
|
|
169
173
|
raw = response.read(limit + 1)
|
|
170
174
|
require(len(raw) <= limit, "RESPONSE_LIMIT", "API 回覆超過大小限制。")
|
|
171
175
|
return json.loads(raw)
|
|
172
176
|
except HTTPError as exc:
|
|
173
177
|
status = exc.code
|
|
174
178
|
exc.close()
|
|
175
|
-
codes = {401: "AUTHENTICATION_FAILED", 403: "ACCESS_DENIED", 404: "RESOURCE_NOT_FOUND", 409: "RESOURCE_CONFLICT", 429: "RATE_LIMITED"}
|
|
179
|
+
codes = {401: "AUTHENTICATION_FAILED", 403: "ACCESS_DENIED", 404: "RESOURCE_NOT_FOUND", 409: "RESOURCE_CONFLICT", 410: "UPLOAD_EXPIRED", 413: "PAYLOAD_LIMIT", 429: "RATE_LIMITED", 507: "STORAGE_CAPACITY"}
|
|
180
|
+
if status == 507: raise FufuError('STORAGE_CAPACITY', 'FUFU 主機磁碟容量不足;這是目前可用空間的限制,並非雲端物件大小上限。') from None
|
|
176
181
|
raise FufuError(codes.get(status, "API_REQUEST_FAILED"), f"FUFU API 拒絕請求(HTTP {status});未輸出憑證或原始回應。") from None
|
|
177
182
|
except (URLError, TimeoutError, ConnectionError) as exc:
|
|
178
183
|
raise FufuError("API_UNREACHABLE", "無法在時限內安全連線至 FUFU API。") from None
|
|
179
184
|
|
|
180
185
|
def request(self, method, path, body=None):
|
|
181
|
-
return self._request(method, f"/api/cloud/{self.provider}" + path, body, large=path.startswith(('/compute/', '/s3/', '/storage/', '/control/native')))
|
|
186
|
+
return self._request(method, f"/api/cloud/{self.provider}" + path, body, large=path.startswith(('/compute/', '/s3/', '/storage/', '/control/native', '/control/pubsub', '/sites')))
|
|
182
187
|
|
|
183
188
|
def whoami(self):
|
|
184
189
|
data = self._request("GET", "/api/auth/whoami")
|
|
185
190
|
return {k: v for k, v in data.items() if k != "credentialId"}
|
|
186
191
|
|
|
192
|
+
def binary(self, method, path, stream=None, length=None):
|
|
193
|
+
require((path.startswith('/objects/') or self.provider == 'cloudflare' and path.startswith(('/sites/', '/r2/')))
|
|
194
|
+
and '\r' not in path and '\n' not in path, 'INVALID_REQUEST', '物件傳輸路徑無效。')
|
|
195
|
+
headers = {'Authorization': 'Bearer '+self.token, 'Content-Type': 'application/octet-stream'}
|
|
196
|
+
if length is not None: headers['Content-Length'] = str(length)
|
|
197
|
+
request = Request(self.endpoint+f'/api/cloud/{self.provider}'+path, data=stream, headers=headers, method=method)
|
|
198
|
+
try:
|
|
199
|
+
response = self.opener.open(request, timeout=300)
|
|
200
|
+
if method == 'GET': return response
|
|
201
|
+
with response:
|
|
202
|
+
return json.loads(response.read(65537))
|
|
203
|
+
except HTTPError as error:
|
|
204
|
+
status = error.code; error.close()
|
|
205
|
+
raise FufuError('OBJECT_TRANSFER_FAILED', f'物件傳輸未完成(HTTP {status});未覆寫下載目的地。') from None
|
|
206
|
+
|
|
187
207
|
def close(self):
|
|
188
208
|
if self.token and self.owns_token:
|
|
189
209
|
try:
|
|
@@ -279,12 +299,11 @@ def remote_command(provider, argv, Parser):
|
|
|
279
299
|
if a.action == "buckets" and a.bucket_action == "list":
|
|
280
300
|
return api.request("GET", "/storage/buckets")
|
|
281
301
|
if a.action == 'cp' and a.source.startswith('gs://'):
|
|
282
|
-
from .
|
|
302
|
+
from .object_transfer import download_file
|
|
283
303
|
require(not a.destination.startswith('gs://'), 'UNSUPPORTED_ARGUMENT', 'cp 目前只支援本機與 Storage 之間傳輸。')
|
|
284
304
|
bucket, key = storage_path(a.source)
|
|
285
305
|
require(bool(key), 'INVALID_RESOURCE', '請指定 object 名稱。')
|
|
286
|
-
|
|
287
|
-
return download_bytes(a.destination, result['contentBase64'])
|
|
306
|
+
return download_file(api, a.destination, bucket, key)
|
|
288
307
|
bucket, key = storage_path(a.destination if a.action == "cp" else a.url)
|
|
289
308
|
path = "/storage/buckets/" + bucket
|
|
290
309
|
if a.action == "buckets":
|
|
@@ -298,5 +317,5 @@ def remote_command(provider, argv, Parser):
|
|
|
298
317
|
return api.request("GET", path + "/object?" + urlencode({"name": key}))
|
|
299
318
|
if a.action == "rm":
|
|
300
319
|
return api.request("DELETE", path + "/object?" + urlencode({"name": key}))
|
|
301
|
-
from .
|
|
302
|
-
return api.
|
|
320
|
+
from .object_transfer import upload_file
|
|
321
|
+
return upload_file(api, a.source, bucket, key)
|