从此

谷歌云 & Google API & Google Cloud Platform(GCP) SDK Client客户端开发接口 | GCS、Firestore、GAE、GAR

综合/最新

使用 Google Cloud 控制台、Google Cloud CLI 或 Firebase 的所有 Google Cloud 用户都必须启用 MFA。

GAR容器清理已内置 https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy?hl=zh-cn

数据格式:
  Google库已包含了GSON库,故快速项目可直接用其解析JSON。

内容

GCP
谷歌云免费主机实例账单中,用量项目名为"E2 Instance Core running in Americas",赠金折扣名则为"E2 Instance Core running free tier discount"。
GAR
登录:
  https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling?hl=zh-cn#token

  命令:
    临时推拉(有效期1小时)
      进入 Google Cloud Shell 执行: gcloud auth print-access-token  或 gcloud auth print-access-token | docker login ...
      echo "" | docker login --username=oauth2accesstoken --password-stdin asia-east1-docker.pkg.dev
      docker pull asia-east1-docker.pkg.dev/project-123456/temp-repo/gcp-faas  或 $GOOGLE_CLOUD_PROJECT

    【GCP中】 sudo gcloud auth configure-docker;  sudo gcloud container images list
    [永远有效/json key file]
        mv project-123456-1234567890ab.json keyfile.json
        docker login -u _json_key -p "$(cat keyfile.json)" asia-east1-docker.pkg.dev
GAE
GAE控制台设置里,可以启用名为"project-123456.appspot.com"的"默认 Cloud Storage 存储桶",属于单区域,我的是东京asia-northeast1,跟GAE一致且无法修改,免费额度为存储5GiB,流出1GiB;同时,还拥有一个上限为5GiB的暂存桶staging.project-123456.appspot.com,虽然免费但按周清空(我是2周),存储桶误删可通过 gcloud beta app repair 重建。

自定义网域:
  域名必须经过 Google Search Console 的验证,才会出现在同一 Google 账号 GAE 的添加列表里,只需添加4个 A 记录就能自动生成 SSL 证书,AAAA 记录则可选。

GAE Gradle Plugin:
  默认用 Jetty 方式的 com.google.cloud.tools.appengine ,其会通过 appengineStage 任务将 appengine-web.xml 生成 app.yaml。
  若用了 apply plugin: 'com.google.cloud.tools.appengine-appyaml',则需要配置 src\main\appengine\app.yaml。
  app.yaml 用于配置非 Jetty 方式网站,比如SpringBoot等,不想写 entrypoint: java -Xmx64m -jar x.war 就得指定下 jar.manifest.attributes.Main-Class。
  session.setAttribute() 后无法 get 解决:true

发邮件每天免费额度5000起步的方法:
  MailServiceFactory.getMailService().sendToAdmins(msg);
  说明 - 入参msg字段to会自动置为null,或用100额度但能指定接收邮箱的send(msg);
GCS
综述:
  自动创建文件夹 - 对象名若包含了目录,不需要专门创建再写入;挂载目录也会自动创建,但子目录则file.exists()返回false。
  对象名长度 - 512 字节(采用 UTF-8 编码),每个汉字占3-4字节,全中文大约128个。

  GCS CORS: [解决javascript fetch跨域/ServiceException: 401 Anonymous caller] 
    [可选] gcloud auth application-default login
    gcloud storage buckets update gs://project-123456.appspot.com --cors-file=cors.json
    vim cors.json
      [{
        "origin": ["https://staticfiles.openle.com", "https://passed.app"],
        "method": ["GET", "HEAD", "POST", "PUT", "DELETE"],
        "responseHeader": ["Content-Type", "x-goog-acl", "x-goog-resumable"],
        "maxAgeSeconds": 3600
      }]
    gcloud storage buckets describe gs://project-123456.appspot.com --format="default(cors_config)"
    gsutil cors get gs://project-123456.appspot.com

JS上传示例:Storage.SignUrlOption.withExtHeaders(Map.of("x-goog-acl", "public-read"));
    var signedUrl = "https://storage.googleapis.com/...";
    fetch(signedUrl, {method: "PUT", body: input.files[0],
      headers: {"x-goog-acl": "public-read"}
    }).then(function () { alert("上传完毕!"); });

JS上传示例:可续传 - Storage.SignUrlOption.withExtHeaders(Map.of("x-goog-resumable", "start"));
    fetch(sUrl, {method: "POST", headers: {'x-goog-resumable': 'start'
      , 'Content-type': 'application/vnd.android.package-archive' // 必设且跟signUrl保持一致。
    }}).then(async resp => { var sUri = resp.headers.get('location');
      for (const chunk of ac) {
        fetch(sUri, {method: "PUT", headers: {'Content-Length': chunk.cFile.size, 'Content-Range': cr}, body: chunk.cFile });
      }
    });

GCS流式响应下载:
    StreamingOutput so = (os) -> {
        try (os) {
            try (var gcs = GcsCommon.gcs()) {
                var bId = BlobId.of("project-123456.appspot.com", objName);
                var blob = gcs.get(bId);
                if (blob != null) {
                    try (var r = gcs.reader(bId)) {
                        Channels.newInputStream(r).transferTo(os);
                    } catch (StorageException se) { se.printStackTrace(System.err); }
                }
            } catch (Exception ex) { ex.printStackTrace(System.err); }
        }
    }; return Response.ok(so).build();
Google Cloud Build
价格 - 每月免费2500分钟,合每天80分钟。

构建:
  说明 - images 所列镜像只会在所有步骤均执行成功后才会推送;若下一步就要使用,可在前一步添加命令 docker push。
  ① 仅 Dockerfile 构建:
FROM busybox CMD ["echo", "$符号无效,只能写死字符串!"]
gcloud builds submit --tag asia-east1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/temp-repo/temp-pkg:cloud-build-test docker run asia-east1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/temp-repo/temp-pkg:cloud-build-test ② yaml配置文件构建(仅build命令(无需push)+images项即自动推送): vim cloudbuild.yaml # 就近访问可改asia.gcr.io
steps: - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/$PROJECT_ID/our-x:1.0.1', '.'] # script: | # docker run ... images: - 'gcr.io/$PROJECT_ID/our-x:1.0.1'
gcloud builds submit --tag asia-east1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/temp-repo/temp-pkg:cloud-build-test --config=cloudbuild.yaml
Google Cloud Run
容器->请求超时:默认500秒,可修改为最高60分钟。
专用环境变量:System.getenv("CLOUD_RUN_TIMEOUT_SECONDS")
账单SKU - CPU Allocation Time(4856-B847-F1EB)、Memory Allocation Time(02A2-9231-36A6)
Google Firestore
综合:
  只有 (default) 数据库有资格使用免费配额:1GiB存储。
  字段值的总大小上限:1 MiB - 89 字节(1048487 字节/汉字349000+个) 在线计算文档总字节数
  字段保留名字前后缀:__.*__
  文档Id字段名:FieldPath.documentId() == "__name__"
  服务器当前时间值:FieldValue.serverTimestamp() 对应文档字段类型为timestamp。
  TTL存留时间字段入参:var dt = OffsetDateTime.now().plusWeeks(1); com.google.cloud.Timestamp.ofTimeSecondsAndNanos(dt.toEpochSecond(), dt.getNano());
  文档创建时间:QueryDocumentSnapshot.getCreateTime()
  根据docId前缀列出:db.collection("c").orderBy(FieldPath.documentId()).startAt(keyPrefix).endAt(keyPrefix + "\uf8ff").get();
  内存中排序:
    var sortList = new ArrayList>();
    var docs = doc.getDocuments(); // 不支持排序,故通过getCreateTime排序后用index索引定位。
    IntStream.range(0, docs.size()).forEach(i -> {
        sortList.add(Map.entry(i, docs.get(i).getCreateTime()));
    });
    sortList.sort(Comparator.comparing(obj -> ((Map.Entry) obj).getValue()).reversed());
    sortList.forEach(kv -> {
        var d = docs.get(kv.getKey());
    });
Gemini
SSE响应流JSON解析:
    String line; // data: {"candidates": [{"content": {"role": "model","parts": [{"text": "The"}]}}]}
    if (line.isBlank() || !line.startsWith("data:")) {
        continue;
    }
    var dataString = line.replaceFirst("data:", "").trim();
    //System.out.println("dataString - " + dataString);
    if (!dataString.isBlank()) {
        try {
            var el = JsonParser.parseString(dataString);
            //System.out.println("el - " + el);
            var rel = el.getAsJsonObject().getAsJsonArray("candidates");
            //System.out.println("rel - " + rel);
            if (rel != null && !rel.isEmpty()) {
                var perts = rel.get(0).getAsJsonObject().getAsJsonObject("content").getAsJsonArray("parts");
                //System.out.println("perts - " + perts);
                if (perts != null && !perts.isEmpty()) {
                    var text = perts.get(0).getAsJsonObject().get("text").getAsString();
                    System.out.println("text: " + text);
                }
            }
        } catch (JsonSyntaxException jse) {
            jse.printStackTrace(System.err);
            continue;
        }
    }
    /* 或 非严谨的正则匹配:
    var pattern = Pattern.compile("\"text\"\\s*:\\s*\"([^\"]+)\"");
    var matcher = pattern.matcher(line.substring(5));
    if (matcher.find()) { System.out.println(matcher.group(1)); }
    */

免费主机/免费实例

GCP免费实例:
  免费区 - 俄勒冈:us-west1(a机房ping值270ms); 爱荷华:us-central1; 南卡罗来纳:us-east1  免费机型只有 e2-micro
  【注意!非默认项】30 GB/月 “标准永久性磁盘”(pd-standard); 1 GB 网络出站流量(不含中国);
  建议勾上“允许 HTTP 流量”、“允许 HTTPS 流量”、“IP 转发”(通常用于路由器和VPN);
  SSH密钥等创建完成后再处理,如果不远程登陆,则完全不需要SSH密钥。

其他

Quarkus:
  纯JDK HttpClient请求:
    说明 - 无需任何Quarkus扩展,即可直接引库使用。
    implementation(platform("com.google.cloud:libraries-bom:26.55.0"))
    implementation("com.google.auth:google-auth-library-oauth2-http")