• R/O
  • SSH
  • HTTPS

catalpa: Commit


Commit MetaInfo

Revision121 (tree)
Time2022-10-27 14:58:35
Authorhirukawa_ryo

Log Message

依存ライブラリ okHttp 3.14.9、rest-client-util 0.3.4 を廃止しました。Netlifyアップロード機能は Java11 HttpClient で実装し直しました。
依存ライブラリ fx-util 0.4.5 -> 0.4.7

Change Summary

Incremental Difference

--- catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/BlogWizard.java (revision 120)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/BlogWizard.java (revision 121)
@@ -38,6 +38,7 @@
3838 import net.osdn.catalpa.addon.blog.Post;
3939 import net.osdn.catalpa.handler.YamlFrontMatterHandler;
4040 import net.osdn.util.io.AutoDetectReader;
41+import net.osdn.util.javafx.Unchecked;
4142 import net.osdn.util.javafx.fxml.Fxml;
4243 import net.osdn.util.javafx.scene.control.DialogEx;
4344
@@ -63,11 +64,17 @@
6364
6465 calendar.getDatePicker().setValue(LocalDate.now());
6566 tfFilename.requestFocus();
66- btnSkip.setOnAction(wrap(this::btnSkip_onAction));
67- btnCreate.setOnAction(wrap(this::btnCreate_onAction));
67+ btnSkip.setOnAction(event -> Unchecked.execute(() -> {
68+ btnSkip_onAction(event);
69+ }));
70+ btnCreate.setOnAction(event -> Unchecked.execute(() -> {
71+ btnCreate_onAction(event);
72+ }));
6873 btnCreate.disableProperty().bind(tfFilename.textProperty().isEmpty());
6974 setResultConverter(param -> { return getResult(); });
70- setOnShown(wrap(this::dialog_onShown));
75+ setOnShown(event -> Unchecked.execute(() -> {
76+ dialog_onShown(event);
77+ }));
7178
7279 // ButtonTypes に何もボタンが追加されていないと、
7380 // ダイアログ右上の×ボタンを押してもダイアログを閉じることができなくなります。
--- catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/LicenseDialog.java (revision 120)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/LicenseDialog.java (revision 121)
@@ -7,11 +7,13 @@
77 import javafx.scene.Cursor;
88 import javafx.scene.Node;
99 import javafx.scene.control.ButtonType;
10+import javafx.scene.control.DialogEvent;
1011 import javafx.scene.control.ScrollPane;
1112 import javafx.scene.control.Separator;
1213 import javafx.scene.text.Text;
1314 import javafx.scene.text.TextFlow;
1415 import javafx.stage.Window;
16+import net.osdn.util.javafx.Unchecked;
1517 import net.osdn.util.javafx.fxml.Fxml;
1618 import net.osdn.util.javafx.scene.control.DialogEx;
1719
@@ -39,7 +41,9 @@
3941 getDialogPane().getButtonTypes().addAll(ButtonType.CLOSE);
4042 scrollPane.prefViewportWidthProperty().bind(textFlow.widthProperty());
4143 scrollPane.prefViewportHeightProperty().bind(textFlow.heightProperty());
42- setOnShown(event -> { Platform.runLater(wrap(this::dialog_onReady)); });
44+ setOnShown(event -> Unchecked.execute(() -> {
45+ dialog_onShown(event);
46+ }));
4347 getDialogPane().getContent().setOpacity(0.0);
4448
4549 Node[] nodes = build(license);
@@ -54,7 +58,7 @@
5458 @FXML ScrollPane scrollPane;
5559 @FXML TextFlow textFlow;
5660
57- void dialog_onReady() throws IOException {
61+ private void dialog_onShown(DialogEvent event) {
5862 getDialogPane().getContent().setOpacity(1.0);
5963 }
6064
--- catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/MainApp.java (revision 120)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/ui/javafx/MainApp.java (revision 121)
@@ -23,8 +23,12 @@
2323 import java.util.List;
2424 import java.util.Map;
2525 import java.util.ResourceBundle;
26+import java.util.concurrent.ExecutorService;
27+import java.util.concurrent.Executors;
28+import java.util.concurrent.TimeUnit;
2629 import java.util.prefs.Preferences;
2730
31+import com.google.api.client.util.store.DataStore;
2832 import freemarker.template.TemplateException;
2933 import javafx.application.Platform;
3034 import javafx.beans.binding.Bindings;
@@ -66,10 +70,10 @@
6670 import net.osdn.catalpa.Util;
6771 import net.osdn.catalpa.upload.UploadConfig;
6872 import net.osdn.catalpa.upload.UploadConfigFactory;
73+import net.osdn.util.javafx.Unchecked;
6974 import net.osdn.util.javafx.application.FxApplicationThread;
7075 import net.osdn.util.javafx.application.SingletonApplication;
71-import net.osdn.util.javafx.concurrent.Async;
72-import net.osdn.util.javafx.event.SilentEventHandler;
76+import net.osdn.util.javafx.concurrent.AsyncTask;
7377 import net.osdn.util.javafx.fxml.Fxml;
7478 import net.osdn.util.javafx.stage.StageUtil;
7579
@@ -78,6 +82,8 @@
7882 public static final String APPLICATION_NAME = "Catalpa";
7983 public static final String APPLICATION_VERSION;
8084
85+ public static ExecutorService serialExecutor = Executors.newSingleThreadExecutor();
86+
8187 private static final String MARKDOWN_CHEAT_SHEET_URL = "https://catalpa.oss.onl/markdown.html";
8288
8389 static {
@@ -111,7 +117,9 @@
111117 uploadConfigFactory = new UploadConfigFactory();
112118
113119 fileWatchService = new FileWatchService();
114- fileWatchService.setOnSucceeded(wrap(this::fileWatchService_onSucceeded));
120+ fileWatchService.setOnSucceeded(event -> Unchecked.execute(() -> {
121+ fileWatchService_onSucceeded(event);
122+ }));
115123
116124 while(HTTP_SERVER_PORT <= HTTP_SERVER_PORT_MAX) {
117125 try {
@@ -143,8 +151,12 @@
143151 Parent root = Fxml.load(this);
144152
145153 Scene scene = new Scene(root);
146- scene.setOnDragOver(wrap(this::scene_onDragOver));
147- scene.setOnDragDropped(wrap(this::scene_onDragDropped));
154+ scene.setOnDragOver(event -> Unchecked.execute(() -> {
155+ scene_onDragOver(event);
156+ }));
157+ scene.setOnDragDropped(event -> Unchecked.execute(() -> {
158+ scene_onDragDropped(event);
159+ }));
148160
149161 StageUtil.setRestorable(primaryStage, Preferences.userNodeForPackage(getClass()));
150162
@@ -232,17 +244,39 @@
232244
233245 @Override
234246 public void initialize(URL location, ResourceBundle resources) {
235- menuFileOpen.setOnAction(wrap(this::btnOpen_onAction));
236- menuFileSaveAs.setOnAction(wrap(this::btnSaveAs_onAction));
237- menuFileExit.setOnAction(wrap(this::menuFileExit_onAction));
238- menuHelpAbout.setOnAction(wrap(this::menuHelpAbout_onAction));
239- lblVSCode.setOnMouseClicked(wrap(this::lblVSCode_onMouseClicked));
240- lblCheatSheet.setOnMouseClicked(wrap(this::lblCheatSheet_onMouseClicked));
241- btnOpen.setOnAction(wrap(this::btnOpen_onAction));
242- btnReload.setOnAction(wrap(this::btnReload_onAction));
243- btnOpenBrowser.setOnAction(wrap(this::btnOpenBrowser_onAction));
244- btnSaveAs.setOnAction(wrap(this::btnSaveAs_onAction));
245- btnUpload.setOnAction(wrap(this::btnUpload_onAction));
247+ menuFileOpen.setOnAction(event -> Unchecked.execute(() -> {
248+ btnOpen_onAction(event);
249+ }));
250+ menuFileSaveAs.setOnAction(event -> Unchecked.execute(() -> {
251+ btnSaveAs_onAction(event);
252+ }));
253+ menuFileExit.setOnAction(event -> Unchecked.execute(() -> {
254+ menuFileExit_onAction(event);
255+ }));
256+ menuHelpAbout.setOnAction(event -> Unchecked.execute(() -> {
257+ menuHelpAbout_onAction(event);
258+ }));
259+ lblVSCode.setOnMouseClicked(event -> Unchecked.execute(() -> {
260+ lblVSCode_onMouseClicked(event);
261+ }));
262+ lblCheatSheet.setOnMouseClicked(event -> Unchecked.execute(() -> {
263+ lblCheatSheet_onMouseClicked(event);
264+ }));
265+ btnOpen.setOnAction(event -> Unchecked.execute(() -> {
266+ btnOpen_onAction(event);
267+ }));
268+ btnReload.setOnAction(event -> Unchecked.execute(() -> {
269+ btnReload_onAction(event);
270+ }));
271+ btnOpenBrowser.setOnAction(event -> Unchecked.execute(() -> {
272+ btnOpenBrowser_onAction(event);
273+ }));
274+ btnSaveAs.setOnAction(event -> Unchecked.execute(() -> {
275+ btnSaveAs_onAction(event);
276+ }));
277+ btnUpload.setOnAction(event -> Unchecked.execute(() -> {
278+ btnUpload_onAction(event);
279+ }));
246280
247281 cbAutoReload.setSelected(preferences.getBoolean("isAutoReload", false));
248282 cbAutoReload.selectedProperty().addListener((observable, oldValue, newValue)-> {
@@ -297,11 +331,25 @@
297331 .then(inputPath)
298332 .otherwise((Path)null));
299333
300- busy.addListener(wrap(this::busy_onChange));
334+ busy.addListener((observable, oldValue, newValue) -> Unchecked.execute(() -> {
335+ busy_onChange(newValue);
336+ }));
301337
302338 updateRecentFile(null);
303339 }
304340
341+ @Override
342+ public void stop() throws Exception {
343+ serialExecutor.shutdown();
344+ try {
345+ if(!serialExecutor.awaitTermination(5000, TimeUnit.MILLISECONDS)) {
346+ System.err.println("serialExecutor.awaitTermination: timeout");
347+ }
348+ } catch(InterruptedException e) {
349+ e.printStackTrace();
350+ }
351+ }
352+
305353 protected void menuFileExit_onAction(ActionEvent event) {
306354 Platform.exit();
307355 }
@@ -430,7 +478,7 @@
430478 if(!busy.get() && DragboardHelper.hasOneDirectory(event.getDragboard())) {
431479 success = true;
432480 Path dir = DragboardHelper.getDirectory(event.getDragboard());
433- Platform.runLater(wrap(() -> {
481+ Platform.runLater(Unchecked.runnable(() -> {
434482 toast.hide();
435483 if(prepareOpening(dir)) {
436484 open(dir);
@@ -454,7 +502,7 @@
454502 // 600ミリ秒以上連続でイベントが発生しなくなってから処理を開始します。
455503 // (たとえばファイルを書き換えると DELETE + CREATE で2回ファイル変更イベントが連続発生することがあります。)
456504 LocalDateTime t = this.lastModified = LocalDateTime.now();
457- FxApplicationThread.runLater(600, wrap(() -> {
505+ FxApplicationThread.runLater(600, Unchecked.runnable(() -> {
458506 if(t.equals(lastModified) && inputPath.getValue() != null) {
459507 if(busy.get()) {
460508 // 更新や保存処理中は更新が検出されても無視します。
@@ -527,7 +575,7 @@
527575 }
528576 previewOutputPath.setValue(null);
529577
530- Async.execute(() -> {
578+ MainApp.serialExecutor.execute(AsyncTask.create(() -> {
531579 Path outputPath = createTemporaryDirectory("preview-htdocs", false);
532580 Catalpa catalpa = new Catalpa(inputPath);
533581 Map<String, Object> options = new HashMap<String, Object>();
@@ -540,11 +588,11 @@
540588 defaultUrl.setValue((String) options.get("_DEFAULT_URL"));
541589 previewOutputPath.setValue(outputPath);
542590 });
543- }).onSucceeded(() -> {
591+ }).onSucceeded(unused -> {
544592 toast.show(Toast.GREEN, "更新プロセスが正常に終了しました", Toast.SHORT);
545- }).onCompleted(state -> {
593+ }).onFinished(state -> {
546594 busy.setValue(false);
547- });
595+ }));
548596 }
549597
550598 protected void save(Path inputPath, Path outputPath) {
@@ -555,15 +603,16 @@
555603
556604 toast.hide();
557605 busy.setValue(true);
558- Async.execute(() -> {
606+
607+ MainApp.serialExecutor.execute(AsyncTask.create(() -> {
559608 Catalpa catalpa = new Catalpa(inputPath);
560609 Map<String, Object> options = new HashMap<String, Object>();
561610 catalpa.process(outputPath, options, this);
562- }).onSucceeded(() -> {
611+ }).onSucceeded(unused -> {
563612 toast.show(Toast.GREEN, "保存しました", outputPath.toString(), Toast.LONG);
564- }).onCompleted(state -> {
613+ }).onFinished(state -> {
565614 busy.setValue(false);
566- });
615+ }));
567616 }
568617
569618 protected void upload(Path inputPath) {
@@ -574,7 +623,8 @@
574623
575624 toast.hide();
576625 busy.setValue(true);
577- Async.execute(() -> {
626+
627+ MainApp.serialExecutor.execute(AsyncTask.create(() -> {
578628 Path outputPath = createTemporaryDirectory("upload-htdocs", true);
579629 Catalpa catalpa = new Catalpa(inputPath);
580630 Map<String, Object> options = new HashMap<String, Object>();
@@ -581,11 +631,11 @@
581631 catalpa.process(outputPath, options, this);
582632 progressOffset = 0.5;
583633 uploadConfig.get().upload(outputPath.toFile(), this);
584- }).onSucceeded(() -> {
634+ }).onSucceeded(unused -> {
585635 toast.show(Toast.GREEN, "アップロードが完了しました", Toast.LONG);
586- }).onCompleted(state -> {
636+ }).onFinished(state -> {
587637 busy.setValue(false);
588- });
638+ }));
589639 }
590640
591641 @Override
@@ -631,7 +681,7 @@
631681 int i = menuFile.getItems().size() - 1;
632682 for(String p : recentFiles) {
633683 MenuItem item = new MenuItem(p);
634- item.setOnAction(SilentEventHandler.wrap(event -> {
684+ item.setOnAction(event -> Unchecked.execute(() -> {
635685 toast.hide();
636686 String text = ((MenuItem)event.getSource()).getText();
637687 if(prepareOpening(Paths.get(text))) {
--- catalpa/trunk/src/main/java/net/osdn/catalpa/upload/firebase/FirebaseUploader.java (revision 120)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/upload/firebase/FirebaseUploader.java (revision 121)
@@ -27,6 +27,7 @@
2727 import java.util.Map;
2828 import java.util.concurrent.CompletableFuture;
2929 import java.util.concurrent.ExecutionException;
30+import java.util.concurrent.ExecutorService;
3031 import java.util.concurrent.Executors;
3132 import java.util.stream.Stream;
3233 import java.util.zip.Deflater;
@@ -67,74 +68,75 @@
6768 Path input = localDirectory.toPath();
6869 Path output = MainApp.createTemporaryDirectory("upload-htdocs-gzipped", true);
6970
71+ MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
7072 String token = getAccessToken(serviceAccountKeyFilePath);
7173
72- MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
73- HttpClient client = HttpClient.newBuilder()
74- .executor(Executors.newFixedThreadPool(6))
75- .build();
74+ try(ExecutorService executor = Executors.newFixedThreadPool(6)) {
75+ HttpClient client = HttpClient.newBuilder()
76+ .executor(executor)
77+ .build();
7678
77- String versionId = createVersionId(client, token, siteId);
79+ String versionId = createVersionId(client, token, siteId);
7880
79- Map<Path, String> files = new LinkedHashMap<>();
80- int len;
81- byte[] buf = new byte[65536];
82- try(Stream<Path> stream = Files.walk(input)) {
83- List<Path> list = stream.toList();
84- for(Path path : list) {
85- if(Files.isDirectory(path)) {
86- Path dirname = input.relativize(path);
87- Files.createDirectories(output.resolve(dirname));
88- } else {
89- Path file = input.relativize(path);
90- try(InputStream in = Files.newInputStream(path);
91- OutputStream out = Files.newOutputStream(output.resolve(file));
92- GZIPOutputStream gzOut = new GZIPOutputStream(out) {{ def.setLevel(Deflater.BEST_SPEED); }}) {
93- while((len = in.read(buf)) > 0) {
94- gzOut.write(buf, 0, len);
81+ Map<Path, String> files = new LinkedHashMap<>();
82+ int len;
83+ byte[] buf = new byte[65536];
84+ try(Stream<Path> stream = Files.walk(input)) {
85+ List<Path> list = stream.toList();
86+ for(Path path : list) {
87+ if(Files.isDirectory(path)) {
88+ Path dirname = input.relativize(path);
89+ Files.createDirectories(output.resolve(dirname));
90+ } else {
91+ Path file = input.relativize(path);
92+ try(InputStream in = Files.newInputStream(path);
93+ OutputStream out = Files.newOutputStream(output.resolve(file));
94+ GZIPOutputStream gzOut = new GZIPOutputStream(out) {{ def.setLevel(Deflater.BEST_SPEED); }}) {
95+ while((len = in.read(buf)) > 0) {
96+ gzOut.write(buf, 0, len);
97+ }
98+ gzOut.finish();
9599 }
96- gzOut.finish();
100+ String hash = getSHA256(sha256, output.resolve(file));
101+ files.put(file, hash);
97102 }
98- String hash = getSHA256(sha256, output.resolve(file));
99- files.put(file, hash);
100103 }
101104 }
102- }
103105
104- PopulateFilesResult populateFilesResult = populateFiles(client, token, siteId, versionId, files);
106+ PopulateFilesResult populateFilesResult = populateFiles(client, token, siteId, versionId, files);
105107
106- if(populateFilesResult.uploadRequiredHashes != null) {
107- progress = 0;
108- maxProgress = populateFilesResult.uploadRequiredHashes.size();
108+ if(populateFilesResult.uploadRequiredHashes != null) {
109+ progress = 0;
110+ maxProgress = populateFilesResult.uploadRequiredHashes.size();
109111
110- Map<String, Path> map = new HashMap<>();
111- for(Map.Entry<Path, String> entry : files.entrySet()) {
112- map.put(entry.getValue(), entry.getKey());
113- }
114- uploadCount = populateFilesResult.uploadRequiredHashes.size();
112+ Map<String, Path> map = new HashMap<>();
113+ for(Map.Entry<Path, String> entry : files.entrySet()) {
114+ map.put(entry.getValue(), entry.getKey());
115+ }
116+ uploadCount = populateFilesResult.uploadRequiredHashes.size();
115117
116- @SuppressWarnings({"rawtypes", "unchecked"})
117- CompletableFuture<HttpResponse<String>>[] futures = new CompletableFuture[uploadCount];
118- int i = 0;
119- for(String uploadRequiredhash : populateFilesResult.uploadRequiredHashes) {
120- Path file = map.get(uploadRequiredhash);
121- String url = populateFilesResult.uploadUrl + "/" + uploadRequiredhash;
118+ @SuppressWarnings({"rawtypes", "unchecked"})
119+ CompletableFuture<HttpResponse<String>>[] futures = new CompletableFuture[uploadCount];
120+ int i = 0;
121+ for(String hash : populateFilesResult.uploadRequiredHashes) {
122+ Path file = map.get(hash);
123+ String url = populateFilesResult.uploadUrl + "/" + hash;
122124
123- CompletableFuture<HttpResponse<String>> future = upload(client, token, url, output.resolve(file));
124- future.thenAccept(response -> {
125- if(response.statusCode() == 200) {
126- this.observer.setProgress(++progress / (double)maxProgress);
127- this.observer.setText("/" + file.toString().replace('\\', '/'));
128- }
129- });
130- futures[i++] = future;
125+ CompletableFuture<HttpResponse<String>> future = upload(client, token, url, output.resolve(file));
126+ future.thenAccept(response -> {
127+ if(response.statusCode() == 200) {
128+ this.observer.setProgress(++progress / (double)maxProgress);
129+ this.observer.setText("/" + file.toString().replace('\\', '/'));
130+ }
131+ });
132+ futures[i++] = future;
133+ }
134+ CompletableFuture.allOf(futures).get();
131135 }
132- CompletableFuture.allOf(futures).get();
136+
137+ finalizeVersion(client, token, siteId, versionId);
138+ releaseVersion(client, token, siteId, versionId);
133139 }
134-
135- finalizeVersion(client, token, siteId, versionId);
136- releaseVersion(client, token, siteId, versionId);
137-
138140 return uploadCount;
139141 }
140142
@@ -146,7 +148,7 @@
146148 .createScoped("https://www.googleapis.com/auth/firebase.hosting");
147149
148150 AccessToken token = credential.refreshAccessToken();
149- return token.getTokenValue();
151+ return token.getTokenValue().replaceFirst("\\.*$", "");
150152 }
151153 }
152154
@@ -191,7 +193,6 @@
191193 StringBuilder sb = new StringBuilder();
192194 sb.append("{\n");
193195 sb.append(" \"files\": {\n");
194-
195196 for(Map.Entry<Path, String> entry : files.entrySet()) {
196197 Path file = entry.getKey();
197198 String hash = entry.getValue();
@@ -224,11 +225,11 @@
224225 }
225226
226227
227- private static CompletableFuture<HttpResponse<String>> upload(HttpClient client, String token, String url, Path path) throws IOException, InterruptedException {
228+ private static CompletableFuture<HttpResponse<String>> upload(HttpClient client, String token, String url, Path file) throws IOException, InterruptedException {
228229 HttpRequest request = HttpRequest.newBuilder(URI.create(url))
229230 .setHeader("Authorization", "Bearer " + token)
230231 .setHeader("Content-Type", "application/octet-stream")
231- .POST(HttpRequest.BodyPublishers.ofFile(path))
232+ .POST(HttpRequest.BodyPublishers.ofFile(file))
232233 .build();
233234
234235 return client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
@@ -267,7 +268,7 @@
267268 }
268269
269270
270- private static String getSHA256(MessageDigest sha256, Path path) throws IOException, NoSuchAlgorithmException {
271+ private static String getSHA256(MessageDigest sha256, Path path) throws IOException {
271272 sha256.reset();
272273
273274 try(InputStream in = Files.newInputStream(path)) {
--- catalpa/trunk/src/main/java/net/osdn/catalpa/upload/netlify/NetlifyConfig.java (revision 120)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/upload/netlify/NetlifyConfig.java (revision 121)
@@ -3,7 +3,6 @@
33 import net.osdn.catalpa.ProgressObserver;
44 import net.osdn.catalpa.ui.javafx.ToastMessage;
55 import net.osdn.catalpa.upload.UploadConfig;
6-import net.osdn.util.rest.client.HttpException;
76
87 import java.io.File;
98
@@ -45,11 +44,7 @@
4544
4645 NetlifyUploader uploader = new NetlifyUploader(this);
4746 int count = 0;
48- try {
49- count = uploader.upload(dir, observer);
50- } catch(HttpException._401_UNAUTHORIZED e) {
51- throw new ToastMessage("Netlify", "認証に失敗しました。\r\npersonalAccessToken が正しいか確認してください。");
52- }
47+ count = uploader.upload(dir, observer);
5348
5449 return count;
5550 }
--- catalpa/trunk/src/main/java/net/osdn/catalpa/upload/netlify/NetlifyUploader.java (revision 120)
+++ catalpa/trunk/src/main/java/net/osdn/catalpa/upload/netlify/NetlifyUploader.java (revision 121)
@@ -1,32 +1,39 @@
11 package net.osdn.catalpa.upload.netlify;
22
3+import com.fasterxml.jackson.core.type.TypeReference;
4+import com.fasterxml.jackson.databind.DeserializationFeature;
5+import com.fasterxml.jackson.databind.ObjectMapper;
36 import net.osdn.catalpa.ProgressObserver;
47 import net.osdn.catalpa.URLEncoder;
58 import net.osdn.catalpa.ui.javafx.MainApp;
69 import net.osdn.catalpa.ui.javafx.ToastMessage;
7-import net.osdn.util.rest.client.HttpException;
8-import net.osdn.util.rest.client.RestClient;
9-import okhttp3.MediaType;
10-import okhttp3.RequestBody;
1110
1211 import java.io.File;
13-import java.io.FileInputStream;
1412 import java.io.IOException;
13+import java.io.InputStream;
1514 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;
1619 import java.nio.charset.StandardCharsets;
1720 import java.nio.file.Files;
1821 import java.nio.file.Path;
1922 import java.security.MessageDigest;
2023 import java.security.NoSuchAlgorithmException;
21-import java.util.ArrayList;
2224 import java.util.HashMap;
25+import java.util.LinkedHashMap;
2326 import java.util.List;
2427 import java.util.Map;
28+import java.util.concurrent.CompletableFuture;
29+import java.util.concurrent.ExecutionException;
30+import java.util.concurrent.ExecutorService;
31+import java.util.concurrent.Executors;
32+import java.util.stream.Stream;
2533
2634 public class NetlifyUploader {
2735
2836 private NetlifyConfig config;
29- private RestClient.Instance netlify;
3037 private MessageDigest sha1;
3138 private Map<String, String> deployPathBySHA = new HashMap<String, String>();
3239
@@ -38,7 +45,7 @@
3845 this.config = config;
3946 }
4047
41- public int upload(File localDirectory, ProgressObserver observer) throws IOException, HttpException, NoSuchAlgorithmException {
48+ public int upload(File localDirectory, ProgressObserver observer) throws IOException, InterruptedException, NoSuchAlgorithmException, ExecutionException {
4249 this.observer = (observer != null) ? observer : ProgressObserver.EMPTY;
4350 this.observer.setProgress(0.0);
4451 this.observer.setText("アップロードの準備をしています…");
@@ -45,41 +52,100 @@
4552
4653 int uploadCount = 0;
4754
48- netlify = RestClient.newInstance();
49- netlify.setUrl("https://api.netlify.com/api/v1");
50- netlify.setAuthorization("Bearer " + config.getPersonalAccessToken());
55+ String siteName = config.getSiteName();
56+ if(siteName == null) {
57+ throw new ToastMessage("Netlify", "siteName が設定されていません。");
58+ }
5159
52- String siteId = getSiteId(config.getSiteName());
53- if(siteId == null) {
54- throw new ToastMessage("Netlify", "サイト名が見つかりません: " + config.getSiteName());
60+ String token = config.getPersonalAccessToken();
61+ if(token == null) {
62+ throw new ToastMessage("Netlify", "personalAccessToken が設定されていません。");
5563 }
5664
57- CreateSiteDeployResult createSiteDeployResult = createSiteDeploy(siteId, localDirectory);
58- String deployId = createSiteDeployResult.id;
59- List<String> required = createSiteDeployResult.required;
65+ Path input = localDirectory.toPath();
6066
61- progress = 0;
62- maxProgress = required.size() + 1;
67+ MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
6368
64- for(String sha : required) {
65- String relativePath = deployPathBySHA.get(sha);
66- Path localFilePath = localDirectory.toPath().resolve(relativePath.substring(1));
69+ try(ExecutorService executor = Executors.newFixedThreadPool(6)) {
70+ HttpClient client = HttpClient.newBuilder()
71+ .executor(executor)
72+ .build();
6773
68- this.observer.setProgress(++progress / (double)maxProgress);
69- this.observer.setText(relativePath.substring(1));
74+ String siteId = getSiteId(client, token, siteName);
75+ if(siteId == null) {
76+ throw new ToastMessage("Netlify", "サイト名が見つかりません: " + siteName);
77+ }
7078
71- uploadDeployFile(deployId, relativePath, localFilePath);
72- uploadCount++;
79+ Map<Path, String> files = new LinkedHashMap<>();
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 file = input.relativize(path);
85+ String hash = getSHA1(sha1, path);
86+ files.put(file, hash);
87+ }
88+ }
89+ }
90+
91+ CreateSiteDeployResult createSiteDeployResult = createSiteDeploy(client, token, siteId, files);
92+ String deployId = createSiteDeployResult.id;
93+ List<String> required = createSiteDeployResult.required;
94+
95+ if(required != null && required.size() > 0) {
96+ progress = 0;
97+ maxProgress = required.size();
98+
99+ Map<String, Path> map = new HashMap<>();
100+ for(Map.Entry<Path, String> entry : files.entrySet()) {
101+ map.put(entry.getValue(), entry.getKey());
102+ }
103+ uploadCount = required.size();
104+
105+ @SuppressWarnings({"rawtypes", "unchecked"})
106+ CompletableFuture<HttpResponse<String>>[] futures = new CompletableFuture[uploadCount];
107+ int i = 0;
108+ for(String hash : required) {
109+ Path file = map.get(hash);
110+ String url = "/" + file.toString().replace('\\', '/');
111+
112+ CompletableFuture<HttpResponse<String>> future = uploadDeployFile(client, token, deployId, url, input.resolve(file));
113+ future.thenAccept(response -> {
114+ if(response.statusCode() == 200) {
115+ this.observer.setProgress(++progress / (double)maxProgress);
116+ this.observer.setText(url);
117+ }
118+ });
119+ futures[i++] = future;
120+ }
121+ CompletableFuture.allOf(futures).get();
122+ }
73123 }
74-
75124 return uploadCount;
76125 }
77126
78- public String getSiteId(String siteName) throws IOException, HttpException {
127+
128+ private static String getSiteId(HttpClient client, String token, String siteName) throws IOException, InterruptedException {
79129 if(siteName == null) {
80130 return null;
81131 }
82- List<ListSitesResult> results = netlify.path("/sites").getList(ListSitesResult.class);
132+
133+ String url = "https://api.netlify.com" + "/api/v1/sites";
134+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
135+ .setHeader("Authorization", "Bearer " + token)
136+ .GET()
137+ .build();
138+
139+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
140+
141+ if(response.statusCode() != 200) {
142+ throw new IOException(response + "\n" + response.body());
143+ }
144+
145+ List<ListSitesResult> results = new ObjectMapper()
146+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
147+ .readValue(response.body(), new TypeReference<List<ListSitesResult>>(){});
148+
83149 for(ListSitesResult result : results) {
84150 if(siteName.equals(result.name)) {
85151 return result.site_id;
@@ -88,75 +154,65 @@
88154 return null;
89155 }
90156
91- public CreateSiteDeployResult createSiteDeploy(String siteId, File localDirectory) throws IOException, NoSuchAlgorithmException, HttpException {
92- String json = createDeployFilesJson(localDirectory);
93157
94- CreateSiteDeployResult result = netlify.reset().path("/sites").path(siteId).path("/deploys")
95- .rawParam("title", "Uploaded from " + MainApp.APPLICATION_NAME + " " + MainApp.APPLICATION_VERSION)
96- .post("application/json", json.getBytes(StandardCharsets.UTF_8))
97- .get(CreateSiteDeployResult.class);
158+ private static CreateSiteDeployResult createSiteDeploy(HttpClient client, String token, String siteId, Map<Path, String> files) throws IOException, InterruptedException {
159+ StringBuilder sb = new StringBuilder();
160+ sb.append("{\n");
161+ sb.append(" \"files\": {\n");
162+ for(Map.Entry<Path, String> entry : files.entrySet()) {
163+ Path file = entry.getKey();
164+ String hash = entry.getValue();
165+ sb.append(" \"/" + file.toString().replace('\\', '/') + "\": ");
166+ sb.append("\"" + hash + "\"");
167+ sb.append(",\n");
168+ }
169+ if(files.size() > 0) {
170+ sb.delete(sb.length() - 2, sb.length());
171+ sb.append("\n");
172+ }
173+ sb.append(" }\n");
174+ sb.append("}\n");
98175
99- return result;
100- }
176+ String url = "https://api.netlify.com" + "/api/v1/sites/" + siteId + "/deploys"
177+ + "?title=" + URLEncoder.encode("Uploaded from " + MainApp.APPLICATION_NAME + " " + MainApp.APPLICATION_VERSION);
178+ HttpRequest request = HttpRequest.newBuilder(URI.create(url))
179+ .setHeader("Authorization", "Bearer " + token)
180+ .setHeader("Content-Type", "application/json")
181+ .POST(HttpRequest.BodyPublishers.ofString(sb.toString()))
182+ .build();
101183
102- private String createDeployFilesJson(File localDirectory) throws IOException, NoSuchAlgorithmException {
103- StringBuilder sb = new StringBuilder();
104- sb.append("{\r\n");
105- sb.append("\t\"files\": {\r\n");
184+ HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
106185
107- int localDirectoryLength = localDirectory.getAbsolutePath().length();
108- List<File> files = listLocalFiles(localDirectory);
109- for(int i = 0; i < files.size(); i++) {
110- File file = files.get(i);
111- String name = file.getAbsolutePath().substring(localDirectoryLength).replace('\\', '/');
112- String sha = getSHA1(file);
113- deployPathBySHA.put(sha, name);
114- sb.append("\t\t\"");
115- sb.append(URLEncoder.encode(name));
116- sb.append("\": \"");
117- sb.append(sha);
118- sb.append("\"");
119- if(i + 1 < files.size()) {
120- sb.append(",");
121- }
122- sb.append("\r\n");
186+ if(response.statusCode() != 200) {
187+ throw new IOException(response + "\n" + response.body());
123188 }
124- sb.append("\t}\r\n");
125- sb.append("}\r\n");
126- return sb.toString();
127- }
128189
129- private String uploadDeployFile(String deployId, String relativePath, Path localFilePath) throws IOException, HttpException {
130- String result = netlify.path("/deploys").path(deployId).path("/files")
131- .path(URLEncoder.encode(relativePath)).param("size", Files.size(localFilePath))
132- .put(RequestBody.create(MediaType.parse("application/octet-stream"), localFilePath.toFile()))
133- .get();
190+ CreateSiteDeployResult result = new ObjectMapper()
191+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
192+ .readValue(response.body(), CreateSiteDeployResult.class);
134193
135194 return result;
136195 }
137196
138- private static List<File> listLocalFiles(File dir) {
139- List<File> list = new ArrayList<File>();
140197
141- for(File child : dir.listFiles()) {
142- if(child.isDirectory()) {
143- list.addAll(listLocalFiles(child));
144- } else {
145- list.add(child);
146- }
147- }
148- return list;
198+ private static CompletableFuture<HttpResponse<String>> uploadDeployFile(HttpClient client, String token, String deployId, String url, Path file) throws IOException {
199+ String _url = "https://api.netlify.com" + "/api/v1/deploys/" + deployId + "/files" + URLEncoder.encode(url)
200+ + "?size=" + Files.size(file);
201+ HttpRequest request = HttpRequest.newBuilder(URI.create(_url))
202+ .setHeader("Authorization", "Bearer " + token)
203+ .setHeader("Content-Type", "application/octet-stream")
204+ .PUT(HttpRequest.BodyPublishers.ofFile(file))
205+ .build();
206+
207+ return client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
149208 }
150209
151210
152- private String getSHA1(File file) throws NoSuchAlgorithmException, IOException {
153- if(sha1 == null) {
154- sha1 = MessageDigest.getInstance("SHA-1");
155- }
211+ private static String getSHA1(MessageDigest sha1, Path path) throws IOException {
156212 sha1.reset();
157213
158- try(FileInputStream in = new FileInputStream(file)) {
159- byte[] buf = new byte[8192];
214+ try(InputStream in = Files.newInputStream(path)) {
215+ byte[] buf = new byte[65536];
160216 int size;
161217 while((size = in.read(buf)) != -1) {
162218 sha1.update(buf, 0, size);
@@ -165,11 +221,13 @@
165221 return String.format("%040x", new BigInteger(1, sha1.digest()));
166222 }
167223
224+
168225 static class ListSitesResult {
169226 public String site_id;
170227 public String name;
171228 }
172229
230+
173231 static class CreateSiteDeployResult {
174232 public String id;
175233 public List<String> required;
Show on old repository browser