• R/O
  • SSH
  • HTTPS

catalpa: Commit


Commit MetaInfo

Revision119 (tree)
Time2022-10-26 18:01:41
Authorhirukawa_ryo

Log Message

Firebase Hosting へのアップロードに対応しました。

Change Summary

Incremental Difference

--- catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/MainApp.java (revision 118)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/MainApp.java (revision 119)
@@ -648,7 +648,7 @@
648648 }
649649 }
650650
651- private static Path createTemporaryDirectory(String dir, boolean isDeleteIfExists) throws IOException {
651+ public static Path createTemporaryDirectory(String dir, boolean isDeleteIfExists) throws IOException {
652652 Path path = Paths.get(System.getProperty("java.io.tmpdir"))
653653 .resolve("catalpa")
654654 .resolve(dir);
--- catalpa/trunk/src/main/java/net/osdn/catalpa/upload/firebase/FirebaseConfig.java (nonexistent)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/upload/firebase/FirebaseConfig.java (revision 119)
@@ -0,0 +1,62 @@
1+package net.osdn.catalpa.upload.firebase;
2+
3+import net.osdn.catalpa.ProgressObserver;
4+import net.osdn.catalpa.upload.UploadConfig;
5+
6+import java.io.File;
7+import java.io.FileNotFoundException;
8+import java.io.UncheckedIOException;
9+import java.nio.file.Files;
10+import java.nio.file.Path;
11+import java.nio.file.Paths;
12+
13+public class FirebaseConfig extends UploadConfig {
14+
15+ private String siteId;
16+ private Path secretKeyFilePath;
17+
18+ public FirebaseConfig() {
19+ }
20+
21+ public String getSiteId() {
22+ return this.siteId;
23+ }
24+
25+ public Path getSecretKeyFilePath() {
26+ return this.secretKeyFilePath;
27+ }
28+
29+ private void initialize() {
30+ String s;
31+
32+ s = getValueAsString("siteId");
33+ if(s == null) {
34+ throw new RuntimeException("siteId not found");
35+ }
36+ this.siteId = s.trim();
37+
38+ s = getValueAsString("secretKey");
39+ if(s != null) {
40+ String secretKey = s.replace('/', '\\');
41+ if(secretKey.length() >= 3 && secretKey.substring(1, 3).equals(":\\")) {
42+ this.secretKeyFilePath = Paths.get(secretKey).toAbsolutePath();
43+ } else {
44+ Path p = getFolderPath("secretKey");
45+ this.secretKeyFilePath = p.resolve(secretKey).toAbsolutePath();
46+ }
47+ if(!Files.exists(this.secretKeyFilePath)) {
48+ throw new UncheckedIOException(new FileNotFoundException(this.secretKeyFilePath.toString()));
49+ }
50+ }
51+ }
52+
53+ public int upload(File dir, ProgressObserver observer) throws Exception {
54+ initialize();
55+
56+ FirebaseUploader uploader = new FirebaseUploader(this);
57+ int count = 0;
58+ count = uploader.upload(dir, observer);
59+
60+ return count;
61+ }
62+}
--- catalpa/trunk/src/main/java/net/osdn/catalpa/upload/firebase/FirebaseUploader.java (nonexistent)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/upload/firebase/FirebaseUploader.java (revision 119)
@@ -0,0 +1,287 @@
1+package net.osdn.catalpa.upload.firebase;
2+
3+import com.fasterxml.jackson.databind.ObjectMapper;
4+import com.google.auth.oauth2.AccessToken;
5+import com.google.auth.oauth2.GoogleCredentials;
6+import net.osdn.catalpa.ProgressObserver;
7+import net.osdn.catalpa.ui.javafx.MainApp;
8+import net.osdn.catalpa.ui.javafx.ToastMessage;
9+
10+import java.io.File;
11+import java.io.IOException;
12+import java.io.InputStream;
13+import java.io.OutputStream;
14+import java.math.BigInteger;
15+import java.net.URI;
16+import java.net.http.HttpClient;
17+import java.net.http.HttpRequest;
18+import java.net.http.HttpResponse;
19+import java.nio.charset.StandardCharsets;
20+import java.nio.file.Files;
21+import java.nio.file.Path;
22+import java.security.MessageDigest;
23+import java.security.NoSuchAlgorithmException;
24+import java.util.HashMap;
25+import java.util.LinkedHashMap;
26+import java.util.List;
27+import java.util.Map;
28+import java.util.stream.Stream;
29+import java.util.zip.Deflater;
30+import java.util.zip.GZIPOutputStream;
31+
32+/* Hosting REST API を使用してサイトにデプロイする
33+ * https://firebase.google.com/docs/hosting/api-deploy?hl=ja#raw-https-request_4
34+ *
35+ */
36+public class FirebaseUploader {
37+
38+ private FirebaseConfig config;
39+ private ProgressObserver observer;
40+ private int progress;
41+ private int maxProgress;
42+
43+ public FirebaseUploader(FirebaseConfig config) {
44+ this.config = config;
45+ }
46+
47+ public int upload(File localDirectory, ProgressObserver observer) throws IOException, InterruptedException, NoSuchAlgorithmException {
48+ this.observer = (observer != null) ? observer : ProgressObserver.EMPTY;
49+ this.observer.setProgress(0.0);
50+ this.observer.setText("アップロードの準備をしています…");
51+
52+ int uploadCount = 0;
53+
54+ String siteId = config.getSiteId();
55+ if(siteId == null) {
56+ throw new ToastMessage("Firebase Hosting", "siteId が指定されていません");
57+ }
58+
59+ Path secretKeyFilePath = config.getSecretKeyFilePath();
60+ if(secretKeyFilePath == null) {
61+ throw new ToastMessage("Firebase Hosting", "secretKeyFilePath が指定されていません");
62+ }
63+
64+ Path input = localDirectory.toPath();
65+ Path output = MainApp.createTemporaryDirectory("upload-htdocs-gzipped", true);
66+
67+ MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
68+ HttpClient client = HttpClient.newBuilder()
69+ .version(HttpClient.Version.HTTP_2)
70+ .followRedirects(HttpClient.Redirect.NORMAL)
71+ .build();
72+
73+ String token = getAccessToken(secretKeyFilePath);
74+
75+ String versionId = createVersionId(client, token, siteId);
76+
77+ Map<Path, String> files = new LinkedHashMap<>();
78+ int len;
79+ byte[] buf = new byte[65536];
80+ try(Stream<Path> stream = Files.walk(input)) {
81+ List<Path> list = stream.toList();
82+ for(Path path : list) {
83+ if(Files.isDirectory(path)) {
84+ Path dirname = input.relativize(path);
85+ Files.createDirectories(output.resolve(dirname));
86+ } else {
87+ Path file = input.relativize(path);
88+ try(InputStream in = Files.newInputStream(path);
89+ OutputStream out = Files.newOutputStream(output.resolve(file));
90+ GZIPOutputStream gzOut = new GZIPOutputStream(out) {{ def.setLevel(Deflater.BEST_SPEED); }}) {
91+ while((len = in.read(buf)) > 0) {
92+ gzOut.write(buf, 0, len);
93+ }
94+ gzOut.finish();
95+ }
96+ String hash = getSHA256(sha256, output.resolve(file));
97+ files.put(file, hash);
98+ }
99+ }
100+ }
101+
102+ PopulateFilesResult populateFilesResult = populateFiles(client, token, siteId, versionId, files);
103+
104+ if(populateFilesResult.uploadRequiredHashes != null) {
105+ progress = 0;
106+ maxProgress = populateFilesResult.uploadRequiredHashes.size();
107+
108+ Map<String, Path> map = new HashMap<>();
109+ for(Map.Entry<Path, String> entry : files.entrySet()) {
110+ map.put(entry.getValue(), entry.getKey());
111+ }
112+ for(String uploadRequiredhash : populateFilesResult.uploadRequiredHashes) {
113+ Path file = map.get(uploadRequiredhash);
114+ String url = populateFilesResult.uploadUrl + "/" + uploadRequiredhash;
115+
116+ this.observer.setProgress(++progress / (double)maxProgress);
117+ this.observer.setText("/" + file.toString().replace('\\', '/'));
118+
119+ upload(client, token, url, output.resolve(file));
120+ uploadCount++;
121+ }
122+ }
123+
124+ finalizeVersion(client, token, siteId, versionId);
125+ releaseVersion(client, token, siteId, versionId);
126+
127+ return uploadCount;
128+ }
129+
130+
131+ private static String getAccessToken(Path secretKeyJsonFile) throws IOException {
132+ try(InputStream in = Files.newInputStream(secretKeyJsonFile)) {
133+ GoogleCredentials credential = GoogleCredentials
134+ .fromStream(in)
135+ .createScoped("https://www.googleapis.com/auth/firebase");
136+
137+ AccessToken token = credential.refreshAccessToken();
138+ return token.getTokenValue();
139+ }
140+ }
141+
142+
143+ private static String createVersionId(HttpClient client, String token, String siteId) throws IOException, InterruptedException {
144+ String versionId = null;
145+
146+ String url = "https://firebasehosting.googleapis.com" + "/v1beta1/sites/" + siteId + "/versions";
147+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
148+ .setHeader("Authorization", "Bearer " + token)
149+ .setHeader("Content-Type", "application/json")
150+ .POST(HttpRequest.BodyPublishers.ofString("""
151+ {
152+ "config": {
153+ "headers": [{
154+ "glob": "**",
155+ "headers": {
156+ "cache-Control": "max-age=1800"
157+ }
158+ }]
159+ }
160+ }
161+ """, StandardCharsets.UTF_8))
162+ .build();
163+
164+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
165+
166+ if(response.statusCode() != 200) {
167+ throw new IOException(response + "\n" + response.body());
168+ }
169+
170+ CreateVersionResult result = new ObjectMapper().readValue(response.body(), CreateVersionResult.class);
171+ if(result.status.equals("CREATED")) {
172+ int i = result.name.indexOf("/versions/");
173+ versionId = result.name.substring(i + "/versions/".length());
174+ }
175+ return versionId;
176+ }
177+
178+
179+ private static PopulateFilesResult populateFiles(HttpClient client, String token, String siteId, String versionId, Map<Path, String> files) throws IOException, InterruptedException {
180+ StringBuilder sb = new StringBuilder();
181+ sb.append("{\n");
182+ sb.append(" \"files\": {\n");
183+
184+ for(Map.Entry<Path, String> entry : files.entrySet()) {
185+ Path file = entry.getKey();
186+ String hash = entry.getValue();
187+ sb.append(" \"/" + file.toString().replace('\\', '/') + "\": ");
188+ sb.append("\"" + hash + "\"");
189+ sb.append(",\n");
190+ }
191+ if(files.size() > 0) {
192+ sb.delete(sb.length() - 2, sb.length());
193+ }
194+ sb.append(" }\n");
195+ sb.append("}\n");
196+
197+ String url = "https://firebasehosting.googleapis.com" + "/v1beta1/sites/" + siteId + "/versions/" + versionId + ":populateFiles";
198+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
199+ .setHeader("Authorization", "Bearer " + token)
200+ .setHeader("Content-Type", "application/json")
201+ .POST(HttpRequest.BodyPublishers.ofString(sb.toString(), StandardCharsets.UTF_8))
202+ .build();
203+
204+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
205+
206+ if(response.statusCode() != 200) {
207+ throw new IOException(response + "\n" + response.body());
208+ }
209+
210+ PopulateFilesResult result = new ObjectMapper().readValue(response.body(), PopulateFilesResult.class);
211+ return result;
212+ }
213+
214+
215+ private static void upload(HttpClient client, String token, String url, Path path) throws IOException, InterruptedException {
216+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
217+ .setHeader("Authorization", "Bearer " + token)
218+ .setHeader("Content-Type", "application/octet-stream")
219+ .POST(HttpRequest.BodyPublishers.ofFile(path))
220+ .build();
221+
222+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
223+
224+ if(response.statusCode() != 200) {
225+ throw new IOException(response + "\n" + response.body());
226+ }
227+ }
228+
229+
230+ private static void finalizeVersion(HttpClient client, String token, String siteId, String versionId) throws IOException, InterruptedException {
231+ String url = "https://firebasehosting.googleapis.com" + "/v1beta1/sites/" + siteId + "/versions/" + versionId + "?update_mask=status";
232+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
233+ .setHeader("Authorization", "Bearer " + token)
234+ .setHeader("Content-Type", "application/json")
235+ .method("PATCH", HttpRequest.BodyPublishers.ofString("{\"status\": \"FINALIZED\"}"))
236+ .build();
237+
238+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
239+
240+ if(response.statusCode() != 200) {
241+ throw new IOException(response + "\n" + response.body());
242+ }
243+ }
244+
245+
246+ private static void releaseVersion(HttpClient client, String token, String siteId, String versionId) throws IOException, InterruptedException {
247+ String url = "https://firebasehosting.googleapis.com" + "/v1beta1/sites/" + siteId + "/releases?versionName=sites/" + siteId + "/versions/" + versionId;
248+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
249+ .setHeader("Authorization", "Bearer " + token)
250+ .setHeader("Content-Type", "application/json")
251+ .POST(HttpRequest.BodyPublishers.noBody())
252+ .build();
253+
254+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
255+
256+ if(response.statusCode() != 200) {
257+ throw new IOException(response + "\n" + response.body());
258+ }
259+ }
260+
261+
262+ private static String getSHA256(MessageDigest sha256, Path path) throws IOException, NoSuchAlgorithmException {
263+ sha256.reset();
264+
265+ try(InputStream in = Files.newInputStream(path)) {
266+ byte[] buf = new byte[65536];
267+ int len;
268+ while((len = in.read(buf)) != -1) {
269+ sha256.update(buf, 0, len);
270+ }
271+ }
272+ return String.format("%040x", new BigInteger(1, sha256.digest()));
273+ }
274+
275+
276+ static class CreateVersionResult {
277+ public String name;
278+ public String status;
279+ public Object config;
280+ }
281+
282+
283+ static class PopulateFilesResult {
284+ public List<String> uploadRequiredHashes;
285+ public String uploadUrl;
286+ }
287+}
--- catalpa/trunk/src/main/java/net/osdn/catalpa/upload/UploadConfigFactory.java (revision 118)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/upload/UploadConfigFactory.java (revision 119)
@@ -11,6 +11,7 @@
1111
1212 import net.osdn.catalpa.Catalpa;
1313 import net.osdn.catalpa.Context;
14+import net.osdn.catalpa.upload.firebase.FirebaseConfig;
1415 import net.osdn.catalpa.upload.netlify.NetlifyConfig;
1516 import net.osdn.catalpa.upload.sftp.SftpConfig;
1617 import net.osdn.catalpa.upload.smb.SmbConfig;
@@ -66,7 +67,9 @@
6667
6768 String type = UploadConfig.getConfigValueAsString(uploadConfigData, "type");
6869 if(type != null) {
69- if(type.equals("netlify")) {
70+ if(type.equals("firebase")) {
71+ uploadConfig = new FirebaseConfig();
72+ } else if(type.equals("netlify")) {
7073 uploadConfig = new NetlifyConfig();
7174 } else if(type.equals("sftp")) {
7275 uploadConfig = new SftpConfig();
Show on old repository browser