File transfer fixes

This commit is contained in:
crschnick 2024-05-24 17:54:54 +00:00
parent d41b3017f6
commit a7d825be67
11 changed files with 237 additions and 155 deletions

View file

@ -0,0 +1,83 @@
package io.xpipe.app.browser;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import java.io.OutputStream;
public class BrowserFileOpener {
public static void openWithAnyApplication(OpenFileSystemModel model, FileSystem.FileEntry entry) {
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
.openIO(
FileNames.getFileName(file),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> {
if (model.isClosed()) {
return OutputStream.nullOutputStream();
}
return entry.getFileSystem().openOutput(file, size);
},
s -> FileOpener.openWithAnyApplication(s));
}
public static void openInDefaultApplication(OpenFileSystemModel model, FileSystem.FileEntry entry) {
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
.openIO(
FileNames.getFileName(file),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> {
if (model.isClosed()) {
return OutputStream.nullOutputStream();
}
return entry.getFileSystem().openOutput(file, size);
},
s -> FileOpener.openInDefaultApplication(s));
}
public static void openInTextEditor(OpenFileSystemModel model, FileSystem.FileEntry entry) {
var editor = AppPrefs.get().externalEditor().getValue();
if (editor == null) {
return;
}
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
.openIO(
FileNames.getFileName(file),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> {
if (model.isClosed()) {
return OutputStream.nullOutputStream();
}
return entry.getFileSystem().openOutput(file, size);
},
FileOpener::openInTextEditor);
}
}

View file

@ -28,7 +28,7 @@ public interface LeafAction extends BrowserAction {
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(model.getBusy(), () -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);
@ -77,7 +77,7 @@ public interface LeafAction extends BrowserAction {
}));
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(model.getBusy(), () -> {
BooleanScope.executeExclusive(model.getBusy(), () -> {
// Start shell in case we exited
model.getFileSystem().getShell().orElseThrow().start();
execute(model, selected);

View file

@ -79,7 +79,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
@Override
public void init() throws Exception {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
var fs = entry.getStore().createFileSystem();
if (fs.getShell().isPresent()) {
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
@ -100,7 +100,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
@Override
public void close() {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
@ -140,7 +140,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
return;
}
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (entry.getStore() instanceof ShellStore s) {
c.accept(fileSystem.getShell().orElseThrow());
if (refresh) {
@ -153,7 +153,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
@SneakyThrows
public void refresh() {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
cdSyncWithoutCheck(currentPath.get());
});
}
@ -339,7 +339,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
public void dropLocalFilesIntoAsync(FileSystem.FileEntry entry, List<Path> files) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
@ -361,7 +361,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
@ -384,7 +384,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
@ -408,7 +408,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
@ -431,7 +431,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
return;
}
@ -466,7 +466,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
return;
}
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem.getShell().isPresent()) {
var connection = fileSystem.getShell().get();
var name = (directory != null ? directory + " - " : "")

View file

@ -1,27 +1,17 @@
package io.xpipe.app.util;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.BooleanProperty;
public class BooleanScope implements AutoCloseable {
private final BooleanProperty prop;
private boolean invert;
private boolean forcePlatform;
private boolean wait;
public BooleanScope(BooleanProperty prop) {
this.prop = prop;
}
public static <E extends Throwable> void execute(BooleanProperty prop, FailableRunnable<E> r) throws E {
try (var ignored = new BooleanScope(prop).start()) {
r.run();
}
}
public static <E extends Throwable> void executeExclusive(BooleanProperty prop, FailableRunnable<E> r) throws E {
try (var ignored = new BooleanScope(prop).exclusive().start()) {
r.run();
@ -33,37 +23,19 @@ public class BooleanScope implements AutoCloseable {
return this;
}
public BooleanScope invert() {
this.invert = true;
return this;
}
public BooleanScope forcePlatform() {
this.forcePlatform = true;
return this;
}
public BooleanScope start() {
public synchronized BooleanScope start() {
if (wait) {
while (!invert == prop.get()) {
while (prop.get()) {
ThreadHelper.sleep(50);
}
}
if (forcePlatform) {
PlatformThread.runLaterIfNeeded(() -> prop.setValue(!invert));
} else {
prop.setValue(!invert);
}
prop.setValue(true);
return this;
}
@Override
public void close() {
if (forcePlatform) {
PlatformThread.runLaterIfNeeded(() -> prop.setValue(invert));
} else {
prop.setValue(invert);
}
public synchronized void close() {
prop.setValue(false);
}
}

View file

@ -7,16 +7,18 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.FailableFunction;
import io.xpipe.core.util.FailableSupplier;
import lombok.Getter;
import org.apache.commons.io.FileUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.function.BiConsumer;
@ -24,6 +26,37 @@ import java.util.function.Consumer;
public class FileBridge {
private static class FixedSizeInputStream extends SimpleFilterInputStream {
private long count;
private final long size;
protected FixedSizeInputStream(InputStream in, long size) {
super(in);
this.size = size;
}
@Override
public int read() throws IOException {
if (count >= size) {
return -1;
}
var read = in.read();
count++;
if (read == -1) {
return 0;
} else {
return read;
}
}
@Override
public int available() throws IOException {
return (int) (size - count);
}
}
private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("bridge");
private static FileBridge INSTANCE;
private final Set<Entry> openEntries = new HashSet<>();
@ -95,16 +128,17 @@ public class FileBridge {
if (e.hasChanged()) {
event("Registering change for file " + TEMP.relativize(e.file) + " for editor entry " + e.getName());
e.registerChange();
var expectedSize = Files.size(e.file);
try (var in = Files.newInputStream(e.file)) {
var actualSize = (long) in.available();
if (expectedSize != actualSize) {
event("Expected file size " + expectedSize + " but got size " + actualSize + ". Ignoring change ...");
return;
var started = Instant.now();
try (var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), actualSize)) {
e.writer.accept(fixedIn, actualSize);
}
e.writer.accept(in, actualSize);
var taken = Duration.between(started, Instant.now());
event("Wrote " + HumanReadableFormat.byteCount(actualSize) + " in " + taken.toMillis() + "ms");
}
} else {
event("File doesn't seem to be changed");
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();
@ -134,45 +168,10 @@ public class FileBridge {
return Optional.empty();
}
public void openReadOnlyString(String input, Consumer<String> fileConsumer) {
if (input == null) {
input = "";
}
var id = UUID.randomUUID();
String s = input;
openIO(
id.toString(),
id,
() -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)),
null,
fileConsumer);
}
public void openString(
String keyName, Object key, String input, Consumer<String> output, Consumer<String> fileConsumer) {
if (input == null) {
input = "";
}
String s = input;
openIO(
keyName,
key,
() -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)),
(size) -> new ByteArrayOutputStream(s.length()) {
@Override
public void close() throws IOException {
super.close();
output.accept(new String(toByteArray(), StandardCharsets.UTF_8));
}
},
fileConsumer);
}
public synchronized void openIO(
String keyName,
Object key,
BooleanScope scope,
FailableSupplier<InputStream> input,
FailableFunction<Long, OutputStream, Exception> output,
Consumer<String> consumer) {
@ -206,12 +205,22 @@ public class FileBridge {
return;
}
var entry = new Entry(file, key, keyName, (in, size) -> {
var entry = new Entry(file, key, keyName, scope, (in, size) -> {
if (output != null) {
try (var out = output.apply(size)) {
in.transferTo(out);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
if (scope != null) {
try (var ignored = scope.start()) {
try (var out = output.apply(size)) {
in.transferTo(out);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
} else {
try (var out = output.apply(size)) {
in.transferTo(out);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
}
}
});
@ -227,13 +236,15 @@ public class FileBridge {
private final Path file;
private final Object key;
private final String name;
private final BooleanScope scope;
private final BiConsumer<InputStream, Long> writer;
private Instant lastModified;
public Entry(Path file, Object key, String name, BiConsumer<InputStream, Long> writer) {
public Entry(Path file, Object key, String name, BooleanScope scope, BiConsumer<InputStream, Long> writer) {
this.file = file;
this.key = key;
this.name = name;
this.scope = scope;
this.writer = writer;
}

View file

@ -5,64 +5,20 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.CommandControl;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.SneakyThrows;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.UUID;
import java.util.function.Consumer;
public class FileOpener {
public static void openWithAnyApplication(FileSystem.FileEntry entry) {
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
.openIO(
FileNames.getFileName(file),
key,
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> entry.getFileSystem().openOutput(file, size),
s -> openWithAnyApplication(s));
}
public static void openInDefaultApplication(FileSystem.FileEntry entry) {
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
.openIO(
FileNames.getFileName(file),
key,
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> entry.getFileSystem().openOutput(file, size),
s -> openInDefaultApplication(s));
}
public static void openInTextEditor(FileSystem.FileEntry entry) {
var editor = AppPrefs.get().externalEditor().getValue();
if (editor == null) {
return;
}
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
.openIO(
FileNames.getFileName(file),
key,
() -> {
return entry.getFileSystem().openInput(file);
},
(size) -> entry.getFileSystem().openOutput(file, size),
FileOpener::openInTextEditor);
}
public static void openInTextEditor(String localFile) {
var editor = AppPrefs.get().externalEditor().getValue();
if (editor == null) {
@ -119,11 +75,40 @@ public class FileOpener {
}
public static void openReadOnlyString(String input) {
FileBridge.get().openReadOnlyString(input, s -> openInTextEditor(s));
if (input == null) {
input = "";
}
var id = UUID.randomUUID();
String s = input;
FileBridge.get().openIO(
id.toString(),
id,
null,
() -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)),
null,
v -> openInTextEditor(v));
}
public static void openString(String keyName, Object key, String input, Consumer<String> output) {
FileBridge.get().openString(keyName, key, input, output, file -> openInTextEditor(file));
if (input == null) {
input = "";
}
String s = input;
FileBridge.get().openIO(
keyName,
key,
null,
() -> new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)),
(size) -> new ByteArrayOutputStream(s.length()) {
@Override
public void close() throws IOException {
super.close();
output.accept(new String(toByteArray(), StandardCharsets.UTF_8));
}
},
file -> openInTextEditor(file));
}
public static void openCommandOutput(String keyName, Object key, CommandControl cc) {
@ -131,6 +116,7 @@ public class FileOpener {
.openIO(
keyName,
key,
null,
() -> new FilterInputStream(cc.getStdout()) {
@Override
@SneakyThrows

View file

@ -110,7 +110,7 @@ public class ScanAlert {
window.close();
});
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
entry.get().get().setExpanded(true);
var copy = new ArrayList<>(selected);
for (var a : copy) {
@ -177,7 +177,7 @@ public class ScanAlert {
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(busy, () -> {
BooleanScope.executeExclusive(busy, () -> {
if (shellControl != null) {
shellControl.close();
shellControl = null;

View file

@ -0,0 +1,30 @@
package io.xpipe.app.util;
import lombok.NonNull;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
public abstract class SimpleFilterInputStream extends FilterInputStream {
protected SimpleFilterInputStream(InputStream in) {
super(in);
}
@Override
public abstract int read() throws IOException;
@Override
public int read(byte @NonNull [] b, int off, int len) throws IOException {
for (int i = off; i < off + len; i++) {
var r = (byte) read();
if (r == -1) {
return i - off == 0 ? -1 : i - off;
}
b[i] = r;
}
return len;
}
}

View file

@ -1,11 +1,11 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserFileOpener;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
@ -28,7 +28,7 @@ public class EditFileAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
for (BrowserEntry entry : entries) {
FileOpener.openInTextEditor(entry.getRawFileEntry());
BrowserFileOpener.openInTextEditor(model, entry.getRawFileEntry());
}
}

View file

@ -1,10 +1,10 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserFileOpener;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.store.FileKind;
import javafx.beans.value.ObservableValue;
@ -22,7 +22,7 @@ public class OpenFileDefaultAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
for (var entry : entries) {
FileOpener.openInDefaultApplication(entry.getRawFileEntry());
BrowserFileOpener.openInDefaultApplication(model, entry.getRawFileEntry());
}
}

View file

@ -1,10 +1,10 @@
package io.xpipe.ext.base.browser;
import io.xpipe.app.browser.BrowserFileOpener;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind;
@ -23,7 +23,7 @@ public class OpenFileWithAction implements LeafAction {
@Override
public void execute(OpenFileSystemModel model, List<BrowserEntry> entries) {
var e = entries.getFirst();
FileOpener.openWithAnyApplication(e.getRawFileEntry());
BrowserFileOpener.openWithAnyApplication(model, e.getRawFileEntry());
}
@Override