Show owner information for files

This commit is contained in:
crschnick 2024-08-17 14:27:59 +00:00
parent 1ebc60cb71
commit ef5427f046
39 changed files with 344 additions and 199 deletions

View file

@ -6,7 +6,7 @@ import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileEntry;
import io.xpipe.core.util.FailableRunnable; import io.xpipe.core.util.FailableRunnable;
import javafx.beans.property.Property; import javafx.beans.property.Property;
@ -69,7 +69,7 @@ public class BrowserClipboard {
@SneakyThrows @SneakyThrows
public static ClipboardContent startDrag( public static ClipboardContent startDrag(
FileSystem.FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) { FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) {
if (selected.isEmpty()) { if (selected.isEmpty()) {
return null; return null;
} }
@ -82,7 +82,7 @@ public class BrowserClipboard {
} }
@SneakyThrows @SneakyThrows
public static void startCopy(FileSystem.FileEntry base, List<BrowserEntry> selected) { public static void startCopy(FileEntry base, List<BrowserEntry> selected) {
if (selected.isEmpty()) { if (selected.isEmpty()) {
currentCopyClipboard.setValue(null); currentCopyClipboard.setValue(null);
return; return;
@ -118,7 +118,7 @@ public class BrowserClipboard {
@Value @Value
public static class Instance { public static class Instance {
UUID uuid; UUID uuid;
FileSystem.FileEntry baseDirectory; FileEntry baseDirectory;
List<BrowserEntry> entries; List<BrowserEntry> entries;
BrowserFileTransferMode mode; BrowserFileTransferMode mode;

View file

@ -5,14 +5,14 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.FileOpener; import io.xpipe.app.util.FileOpener;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import java.io.OutputStream; import java.io.OutputStream;
public class BrowserFileOpener { public class BrowserFileOpener {
public static void openWithAnyApplication(OpenFileSystemModel model, FileSystem.FileEntry entry) { public static void openWithAnyApplication(OpenFileSystemModel model, FileEntry entry) {
var file = entry.getPath(); var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get() FileBridge.get()
@ -33,7 +33,7 @@ public class BrowserFileOpener {
s -> FileOpener.openWithAnyApplication(s)); s -> FileOpener.openWithAnyApplication(s));
} }
public static void openInDefaultApplication(OpenFileSystemModel model, FileSystem.FileEntry entry) { public static void openInDefaultApplication(OpenFileSystemModel model, FileEntry entry) {
var file = entry.getPath(); var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode(); var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get() FileBridge.get()
@ -54,7 +54,7 @@ public class BrowserFileOpener {
s -> FileOpener.openInDefaultApplication(s)); s -> FileOpener.openInDefaultApplication(s));
} }
public static void openInTextEditor(OpenFileSystemModel model, FileSystem.FileEntry entry) { public static void openInTextEditor(OpenFileSystemModel model, FileEntry entry) {
var editor = AppPrefs.get().externalEditor().getValue(); var editor = AppPrefs.get().externalEditor().getValue();
if (editor == null) { if (editor == null) {
return; return;

View file

@ -10,7 +10,7 @@ import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileEntry;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -40,10 +40,10 @@ public class BrowserOverviewComp extends SimpleComp {
ShellControl sc = model.getFileSystem().getShell().orElseThrow(); ShellControl sc = model.getFileSystem().getShell().orElseThrow();
var commonPlatform = FXCollections.<FileSystem.FileEntry>observableArrayList(); var commonPlatform = FXCollections.<FileEntry>observableArrayList();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
var common = sc.getOsType().determineInterestingPaths(sc).stream() var common = sc.getOsType().determineInterestingPaths(sc).stream()
.map(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s)) .map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.filter(entry -> { .filter(entry -> {
try { try {
return sc.getShellDialect() return sc.getShellDialect()
@ -63,15 +63,16 @@ public class BrowserOverviewComp extends SimpleComp {
var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview) var commonPane = new SimpleTitledPaneComp(AppI18n.observable("common"), commonOverview)
.apply(struc -> VBox.setVgrow(struc.get(), Priority.NEVER)); .apply(struc -> VBox.setVgrow(struc.get(), Priority.NEVER));
var roots = sc.getShellDialect() var roots = model.getFileSystem()
.listRoots(sc) .listRoots()
.map(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s)) .stream()
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.toList(); .toList();
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true) var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true)
.mapped(s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())) .mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
.getList(); .getList();
var recentOverview = new BrowserFileOverviewComp(model, recent, true); var recentOverview = new BrowserFileOverviewComp(model, recent, true);
var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview); var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview);

View file

@ -7,7 +7,7 @@ import java.util.List;
public class BrowserActionFormatter { public class BrowserActionFormatter {
public static String filesArgument(List<BrowserEntry> entries) { public static String filesArgument(List<BrowserEntry> entries) {
return entries.size() == 1 ? entries.getFirst().getOptionallyQuotedFileName() : "(" + entries.size() + ")"; return entries.size() == 1 ? entries.getFirst().getFileName() : "(" + entries.size() + ")";
} }
public static String centerEllipsis(String input, int length) { public static String centerEllipsis(String input, int length) {

View file

@ -2,9 +2,9 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonBar;
@ -45,7 +45,7 @@ public class BrowserAlerts {
.orElse(FileConflictChoice.CANCEL); .orElse(FileConflictChoice.CANCEL);
} }
public static boolean showMoveAlert(List<FileSystem.FileEntry> source, FileSystem.FileEntry target) { public static boolean showMoveAlert(List<FileEntry> source, FileEntry target) {
if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) { if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
return true; return true;
} }
@ -61,7 +61,7 @@ public class BrowserAlerts {
.orElse(false); .orElse(false);
} }
public static boolean showDeleteAlert(List<FileSystem.FileEntry> source) { public static boolean showDeleteAlert(List<FileEntry> source) {
if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) { if (source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
return true; return true;
} }
@ -77,7 +77,7 @@ public class BrowserAlerts {
.orElse(false); .orElse(false);
} }
private static String getSelectedElementsString(List<FileSystem.FileEntry> source) { private static String getSelectedElementsString(List<FileEntry> source) {
var namesHeader = AppI18n.get("selectedElements"); var namesHeader = AppI18n.get("selectedElements");
var names = namesHeader + "\n" var names = namesHeader + "\n"
+ source.stream() + source.stream()

View file

@ -2,28 +2,27 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType; import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType; import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter; import lombok.Getter;
@Getter @Getter
public class BrowserEntry { public class BrowserEntry {
private final BrowserFileListModel model; private final BrowserFileListModel model;
private final FileSystem.FileEntry rawFileEntry; private final FileEntry rawFileEntry;
private final BrowserIconFileType fileType; private final BrowserIconFileType fileType;
private final BrowserIconDirectoryType directoryType; private final BrowserIconDirectoryType directoryType;
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model) { public BrowserEntry(FileEntry rawFileEntry, BrowserFileListModel model) {
this.rawFileEntry = rawFileEntry; this.rawFileEntry = rawFileEntry;
this.model = model; this.model = model;
this.fileType = fileType(rawFileEntry); this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry); this.directoryType = directoryType(rawFileEntry);
} }
private static BrowserIconFileType fileType(FileSystem.FileEntry rawFileEntry) { private static BrowserIconFileType fileType(FileEntry rawFileEntry) {
if (rawFileEntry == null) { if (rawFileEntry == null) {
return null; return null;
} }
@ -42,7 +41,7 @@ public class BrowserEntry {
return null; return null;
} }
private static BrowserIconDirectoryType directoryType(FileSystem.FileEntry rawFileEntry) { private static BrowserIconDirectoryType directoryType(FileEntry rawFileEntry) {
if (rawFileEntry == null) { if (rawFileEntry == null) {
return null; return null;
} }
@ -74,11 +73,6 @@ public class BrowserEntry {
} }
public String getFileName() { public String getFileName() {
return getRawFileEntry().getName(); return FileNames.getFileName(getRawFileEntry().getPath());
}
public String getOptionallyQuotedFileName() {
var n = getFileName();
return FileNames.quoteIfNecessary(n);
} }
} }

View file

@ -1,5 +1,7 @@
package io.xpipe.app.browser.file; package io.xpipe.app.browser.file;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
@ -8,10 +10,10 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
@ -32,9 +34,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
@ -58,6 +57,7 @@ public final class BrowserFileListComp extends SimpleComp {
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;
private final StringProperty typedSelection = new SimpleStringProperty(""); private final StringProperty typedSelection = new SimpleStringProperty("");
private final DoubleProperty ownerWidth = new SimpleDoubleProperty();
public BrowserFileListComp(BrowserFileListModel fileList) { public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList; this.fileList = fileList;
@ -103,13 +103,24 @@ public final class BrowserFileListComp extends SimpleComp {
var modeCol = new TableColumn<BrowserEntry, String>(); var modeCol = new TableColumn<BrowserEntry, String>();
modeCol.textProperty().bind(AppI18n.observable("attributes")); modeCol.textProperty().bind(AppI18n.observable("attributes"));
modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>( modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getMode())); param.getValue().getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u ? u.getPermissions() : null));
modeCol.setCellFactory(col -> new FileModeCell()); modeCol.setCellFactory(col -> new FileModeCell());
modeCol.setResizable(false); modeCol.setResizable(false);
modeCol.setPrefWidth(120); modeCol.setPrefWidth(120);
modeCol.setSortable(false); modeCol.setSortable(false);
modeCol.setReorderable(false); modeCol.setReorderable(false);
var ownerCol = new TableColumn<BrowserEntry, String>();
ownerCol.textProperty().bind(AppI18n.observable("owner"));
ownerCol.setCellValueFactory(param -> {
return new SimpleObjectProperty<>(formatOwner(param.getValue()));
});
ownerCol.setCellFactory(col -> new FileOwnerCell());
ownerCol.setSortable(false);
ownerCol.setReorderable(false);
ownerCol.prefWidthProperty().bind(ownerWidth);
ownerCol.setResizable(false);
var table = new TableView<BrowserEntry>(); var table = new TableView<BrowserEntry>();
table.setAccessibleText("Directory contents"); table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region()); table.setPlaceholder(new Region());
@ -121,18 +132,39 @@ public final class BrowserFileListComp extends SimpleComp {
fileList.setComparator(table.getComparator()); fileList.setComparator(table.getComparator());
return true; return true;
}); });
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_NEXT_COLUMN);
table.setFixedCellSize(32.0); table.setFixedCellSize(32.0);
table.widthProperty().subscribe((newValue) -> {
ownerCol.setVisible(newValue.doubleValue() > 1000);
});
prepareTableSelectionModel(table); prepareTableSelectionModel(table);
prepareTableShortcuts(table); prepareTableShortcuts(table);
prepareTableEntries(table); prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol); prepareTableChanges(table, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(table); prepareTypedSelectionModel(table);
return table; return table;
} }
private String formatOwner(BrowserEntry param) {
FileInfo.Unix unix = param.getRawFileEntry().resolved().getInfo() instanceof FileInfo.Unix u ? u : null;
if (unix == null) {
return null;
}
var m = fileList.getFileSystemModel();
var user = unix.getUser() != null ? unix.getUser() : m.getCache().getUsers().get(unix.getUid());
var group = unix.getGroup() != null ? unix.getGroup() : m.getCache().getGroups().get(unix.getGid());
var uid = String.valueOf(unix.getUid() != null ? unix.getUid() : m.getCache().getUidForUser(user)).replaceAll("000$", "k");
var gid = String.valueOf(unix.getGid() != null ? unix.getGid() : m.getCache().getGidForGroup(group)).replaceAll("000$", "k");
if (uid.equals(gid)) {
return user + " [" + uid + "]";
}
return user + " [" + uid + "] / " + group + " [" + gid + "]";
}
private void prepareTypedSelectionModel(TableView<BrowserEntry> table) { private void prepareTypedSelectionModel(TableView<BrowserEntry> table) {
AtomicReference<Instant> lastFail = new AtomicReference<>(); AtomicReference<Instant> lastFail = new AtomicReference<>();
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
@ -369,8 +401,9 @@ public final class BrowserFileListComp extends SimpleComp {
private void prepareTableChanges( private void prepareTableChanges(
TableView<BrowserEntry> table, TableView<BrowserEntry> table,
TableColumn<BrowserEntry, Instant> mtimeCol, TableColumn<BrowserEntry, Instant> mtimeCol,
TableColumn<BrowserEntry, String> modeCol) { TableColumn<BrowserEntry, String> modeCol,
var lastDir = new SimpleObjectProperty<FileSystem.FileEntry>(); TableColumn<BrowserEntry, String> ownerCol) {
var lastDir = new SimpleObjectProperty<FileEntry>();
Runnable updateHandler = () -> { Runnable updateHandler = () -> {
Platform.runLater(() -> { Platform.runLater(() -> {
var newItems = new ArrayList<>(fileList.getShown().getValue()); var newItems = new ArrayList<>(fileList.getShown().getValue());
@ -386,18 +419,28 @@ public final class BrowserFileListComp extends SimpleComp {
} }
} }
ownerWidth.set(fileList.getAll().getValue().stream()
.map(browserEntry -> formatOwner(browserEntry))
.map(s -> s != null ? s.length() * 10 : 0)
.max(Comparator.naturalOrder()).orElse(150));
if (fileList.getFileSystemModel().getFileSystem() != null) { if (fileList.getFileSystemModel().getFileSystem() != null) {
var shell = fileList.getFileSystemModel() var shell = fileList.getFileSystemModel()
.getFileSystem() .getFileSystem()
.getShell() .getShell()
.orElseThrow(); .orElseThrow();
var hasAttributes = !OsType.WINDOWS.equals(shell.getOsType()); var notWindows = !OsType.WINDOWS.equals(shell.getOsType());
if (!hasAttributes) { if (!notWindows) {
table.getColumns().remove(modeCol); table.getColumns().remove(modeCol);
table.getColumns().remove(ownerCol);
} else { } else {
if (!table.getColumns().contains(modeCol)) { if (!table.getColumns().contains(modeCol)) {
table.getColumns().add(modeCol); table.getColumns().add(modeCol);
} }
if (!table.getColumns().contains(ownerCol)) {
table.getColumns().add(table.getColumns().size() - 1, ownerCol);
} else {
table.getColumns().remove(ownerCol);
}
} }
} }
@ -495,6 +538,23 @@ public final class BrowserFileListComp extends SimpleComp {
} }
} }
private static class FileOwnerCell extends TableCell<BrowserEntry, String> {
public FileOwnerCell() {
setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
}
@Override
protected void updateItem(String owner, boolean empty) {
super.updateItem(owner, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
setText(owner);
}
}
}
private static class FileTimeCell extends TableCell<BrowserEntry, Instant> { private static class FileTimeCell extends TableCell<BrowserEntry, Instant> {
@Override @Override
@ -648,7 +708,7 @@ public final class BrowserFileListComp extends SimpleComp {
.getPath() .getPath()
: getTableRow().getItem().getFileName(); : getTableRow().getItem().getFileName();
var fileName = normalName; var fileName = normalName;
var hidden = getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith("."); var hidden = getTableRow().getItem().getRawFileEntry().getInfo().explicitlyHidden() || fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(HIDDEN, hidden); getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName); text.set(fileName);
// Visibility seems to be bugged, so use opacity // Visibility seems to be bugged, so use opacity

View file

@ -3,9 +3,9 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -55,7 +55,7 @@ public final class BrowserFileListModel {
}); });
} }
public void setAll(Stream<FileSystem.FileEntry> newFiles) { public void setAll(Stream<FileEntry> newFiles) {
try (var s = newFiles) { try (var s = newFiles) {
var l = s.filter(entry -> entry != null) var l = s.filter(entry -> entry != null)
.map(entry -> new BrowserEntry(entry, this)) .map(entry -> new BrowserEntry(entry, this))

View file

@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileEntry;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -26,12 +26,12 @@ import java.util.function.Function;
public class BrowserFileOverviewComp extends SimpleComp { public class BrowserFileOverviewComp extends SimpleComp {
OpenFileSystemModel model; OpenFileSystemModel model;
ObservableList<FileSystem.FileEntry> list; ObservableList<FileEntry> list;
boolean grow; boolean grow;
@Override @Override
protected Region createSimple() { protected Region createSimple() {
Function<FileSystem.FileEntry, Comp<?>> factory = entry -> { Function<FileEntry, Comp<?>> factory = entry -> {
return Comp.of(() -> { return Comp.of(() -> {
var icon = BrowserIcons.createIcon(entry); var icon = BrowserIcons.createIcon(entry);
var graphic = new HorizontalComp(List.of( var graphic = new HorizontalComp(List.of(

View file

@ -2,10 +2,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserTransferProgress; import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.*;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
@ -18,8 +15,8 @@ import java.util.function.Consumer;
public class BrowserFileTransferOperation { public class BrowserFileTransferOperation {
private final FileSystem.FileEntry target; private final FileEntry target;
private final List<FileSystem.FileEntry> files; private final List<FileEntry> files;
private final BrowserFileTransferMode transferMode; private final BrowserFileTransferMode transferMode;
private final boolean checkConflicts; private final boolean checkConflicts;
private final Consumer<BrowserTransferProgress> progress; private final Consumer<BrowserTransferProgress> progress;
@ -27,8 +24,8 @@ public class BrowserFileTransferOperation {
BrowserAlerts.FileConflictChoice lastConflictChoice; BrowserAlerts.FileConflictChoice lastConflictChoice;
public BrowserFileTransferOperation( public BrowserFileTransferOperation(
FileSystem.FileEntry target, FileEntry target,
List<FileSystem.FileEntry> files, List<FileEntry> files,
BrowserFileTransferMode transferMode, BrowserFileTransferMode transferMode,
boolean checkConflicts, boolean checkConflicts,
Consumer<BrowserTransferProgress> progress) { Consumer<BrowserTransferProgress> progress) {
@ -40,7 +37,7 @@ public class BrowserFileTransferOperation {
} }
public static BrowserFileTransferOperation ofLocal( public static BrowserFileTransferOperation ofLocal(
FileSystem.FileEntry target, FileEntry target,
List<Path> files, List<Path> files,
BrowserFileTransferMode transferMode, BrowserFileTransferMode transferMode,
boolean checkConflicts, boolean checkConflicts,
@ -133,7 +130,7 @@ public class BrowserFileTransferOperation {
} }
} }
private void handleSingleOnSameFileSystem(FileSystem.FileEntry source) throws Exception { private void handleSingleOnSameFileSystem(FileEntry source) throws Exception {
// Prevent dropping directory into itself // Prevent dropping directory into itself
if (source.getPath().equals(target.getPath())) { if (source.getPath().equals(target.getPath())) {
return; return;
@ -163,12 +160,12 @@ public class BrowserFileTransferOperation {
} }
} }
private void handleSingleAcrossFileSystems(FileSystem.FileEntry source) throws Exception { private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
if (target.getKind() != FileKind.DIRECTORY) { if (target.getKind() != FileKind.DIRECTORY) {
throw new IllegalStateException("Target " + target.getPath() + " is not a directory"); throw new IllegalStateException("Target " + target.getPath() + " is not a directory");
} }
var flatFiles = new LinkedHashMap<FileSystem.FileEntry, String>(); var flatFiles = new LinkedHashMap<FileEntry, String>();
// Prevent dropping directory into itself // Prevent dropping directory into itself
if (source.getFileSystem().equals(target.getFileSystem()) if (source.getFileSystem().equals(target.getFileSystem())
@ -182,8 +179,8 @@ public class BrowserFileTransferOperation {
flatFiles.put(source, directoryName); flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath()); List<FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileSystem.FileEntry fileEntry : list) { for (FileEntry fileEntry : list) {
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())); var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
flatFiles.put(fileEntry, rel); flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) { if (fileEntry.getKind() == FileKind.FILE) {
@ -225,7 +222,7 @@ public class BrowserFileTransferOperation {
} }
private void transfer( private void transfer(
FileSystem.FileEntry sourceFile, FileEntry sourceFile,
String targetFile, String targetFile,
AtomicLong transferred, AtomicLong transferred,
AtomicLong totalSize, AtomicLong totalSize,
@ -297,14 +294,14 @@ public class BrowserFileTransferOperation {
} }
} }
private void deleteSingle(FileSystem.FileEntry source) throws Exception { private void deleteSingle(FileEntry source) throws Exception {
source.getFileSystem().delete(source.getPath()); source.getFileSystem().delete(source.getPath());
} }
private static final int DEFAULT_BUFFER_SIZE = 1024; private static final int DEFAULT_BUFFER_SIZE = 1024;
private void transferFile( private void transferFile(
FileSystem.FileEntry sourceFile, FileEntry sourceFile,
InputStream inputStream, InputStream inputStream,
OutputStream outputStream, OutputStream outputStream,
AtomicLong transferred, AtomicLong transferred,

View file

@ -6,8 +6,8 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.util.BooleanAnimationTimer; import io.xpipe.app.util.BooleanAnimationTimer;
import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -92,7 +92,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
} }
private List<MenuItem> updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception { private List<MenuItem> updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception {
List<FileSystem.FileEntry> list = new ArrayList<>(); List<FileEntry> list = new ArrayList<>();
model.withFiles(entry.getRawFileEntry().resolved().getPath(), newFiles -> { model.withFiles(entry.getRawFileEntry().resolved().getPath(), newFiles -> {
try (var s = newFiles) { try (var s = newFiles) {
var l = s.map(fileEntry -> fileEntry.resolved()).toList(); var l = s.map(fileEntry -> fileEntry.resolved()).toList();

View file

@ -3,6 +3,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
@ -125,19 +126,17 @@ public class FileSystemHelper {
} }
} }
public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception { public static FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
return new FileSystem.FileEntry( return new FileEntry(
fileSystem, fileSystem,
file, file,
Instant.now(), Instant.now(),
false,
false,
fileSystem.getFileSize(file), fileSystem.getFileSize(file),
null, null,
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE); fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
} }
public static void delete(List<FileSystem.FileEntry> files) { public static void delete(List<FileEntry> files) {
if (files.isEmpty()) { if (files.isEmpty()) {
return; return;
} }

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser.file; package io.xpipe.app.browser.file;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.LocalStore;
@ -18,17 +19,15 @@ public class LocalFileSystem {
} }
} }
public static FileSystem.FileEntry getLocalFileEntry(Path file) throws Exception { public static FileEntry getLocalFileEntry(Path file) throws Exception {
if (localFileSystem == null) { if (localFileSystem == null) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
return new FileSystem.FileEntry( return new FileEntry(
localFileSystem.open(), localFileSystem.open(),
file.toString(), file.toString(),
Files.getLastModifiedTime(file).toInstant(), Files.getLastModifiedTime(file).toInstant(),
Files.isHidden(file),
Files.isExecutable(file),
Files.size(file), Files.size(file),
null, null,
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE); Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);

View file

@ -1,16 +1,23 @@
package io.xpipe.app.browser.fs; package io.xpipe.app.browser.fs;
import io.xpipe.app.util.ShellControlCache; import io.xpipe.app.util.ShellControlCache;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect; import io.xpipe.core.process.ShellDialect;
import lombok.Getter; import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
@Getter @Getter
public class OpenFileSystemCache extends ShellControlCache { public class OpenFileSystemCache extends ShellControlCache {
private final OpenFileSystemModel model; private final OpenFileSystemModel model;
private final String username; private final String username;
private final Map<Integer, String> users = new HashMap<>();
private final Map<Integer, String> groups = new HashMap<>();
public OpenFileSystemCache(OpenFileSystemModel model) throws Exception { public OpenFileSystemCache(OpenFileSystemModel model) throws Exception {
super(model.getFileSystem().getShell().orElseThrow()); super(model.getFileSystem().getShell().orElseThrow());
@ -20,6 +27,42 @@ public class OpenFileSystemCache extends ShellControlCache {
ShellDialect d = sc.getShellDialect(); ShellDialect d = sc.getShellDialect();
// If there is no id command, we should still be fine with just assuming root // If there is no id command, we should still be fine with just assuming root
username = d.printUsernameCommand(sc).readStdoutIfPossible().orElse("root"); username = d.printUsernameCommand(sc).readStdoutIfPossible().orElse("root");
loadUsers();
loadGroups();
}
public int getUidForUser(String name) {
return users.entrySet().stream().filter(e -> e.getValue().equals(name)).findFirst().map(e -> e.getKey()).orElse(0);
}
public int getGidForGroup(String name) {
return groups.entrySet().stream().filter(e -> e.getValue().equals(name)).findFirst().map(e -> e.getKey()).orElse(0);
}
private void loadUsers() throws Exception {
var sc = model.getFileSystem().getShell().orElseThrow();
if (sc.getOsType() == OsType.WINDOWS) {
return;
}
var lines = sc.command(CommandBuilder.of().add("cat").addFile("/etc/passwd")).readStdoutOrThrow();
lines.lines().forEach(s -> {
var split = s.split(":");
users.put(Integer.parseInt(split[2]), split[0]);
});
}
private void loadGroups() throws Exception {
var sc = model.getFileSystem().getShell().orElseThrow();
if (sc.getOsType() == OsType.WINDOWS) {
return;
}
var lines = sc.command(CommandBuilder.of().add("cat").addFile("/etc/group")).readStdoutOrThrow();
lines.lines().forEach(s -> {
var split = s.split(":");
groups.put(Integer.parseInt(split[2]), split[0]);
});
} }
public boolean isRoot() { public boolean isRoot() {

View file

@ -166,7 +166,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
cdSyncWithoutCheck(currentPath.get()); cdSyncWithoutCheck(currentPath.get());
} }
public FileSystem.FileEntry getCurrentParentDirectory() { public FileEntry getCurrentParentDirectory() {
var current = getCurrentDirectory(); var current = getCurrentDirectory();
if (current == null) { if (current == null) {
return null; return null;
@ -177,10 +177,10 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
return null; return null;
} }
return new FileSystem.FileEntry(fileSystem, parent, null, false, false, 0, null, FileKind.DIRECTORY); return new FileEntry(fileSystem, parent, null, 0, null, FileKind.DIRECTORY);
} }
public FileSystem.FileEntry getCurrentDirectory() { public FileEntry getCurrentDirectory() {
if (currentPath.get() == null) { if (currentPath.get() == null) {
return null; return null;
} }
@ -189,7 +189,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
return null; return null;
} }
return new FileSystem.FileEntry(fileSystem, currentPath.get(), null, false, false, 0, null, FileKind.DIRECTORY); return new FileEntry(fileSystem, currentPath.get(), null, 0, null, FileKind.DIRECTORY);
} }
public void cdAsync(String path) { public void cdAsync(String path) {
@ -305,7 +305,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
loadFilesSync(path); loadFilesSync(path);
} }
public void withFiles(String dir, FailableConsumer<Stream<FileSystem.FileEntry>, Exception> consumer) public void withFiles(String dir, FailableConsumer<Stream<FileEntry>, Exception> consumer)
throws Exception { throws Exception {
BooleanScope.executeExclusive(busy, () -> { BooleanScope.executeExclusive(busy, () -> {
if (dir != null) { if (dir != null) {
@ -341,7 +341,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
} }
} }
public void dropLocalFilesIntoAsync(FileSystem.FileEntry entry, List<Path> files) { public void dropLocalFilesIntoAsync(FileEntry entry, List<Path> files) {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> { BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) { if (fileSystem == null) {
@ -358,7 +358,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
} }
public void dropFilesIntoAsync( public void dropFilesIntoAsync(
FileSystem.FileEntry target, List<FileSystem.FileEntry> files, BrowserFileTransferMode mode) { FileEntry target, List<FileEntry> files, BrowserFileTransferMode mode) {
// We don't have to do anything in this case // We don't have to do anything in this case
if (files.isEmpty()) { if (files.isEmpty()) {
return; return;

View file

@ -1,9 +1,10 @@
package io.xpipe.app.browser.icon; package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources; import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.FileNames;
import lombok.Getter; import lombok.Getter;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -36,12 +37,12 @@ public abstract class BrowserIconDirectoryType {
} }
@Override @Override
public boolean matches(FileSystem.FileEntry entry) { public boolean matches(FileEntry entry) {
return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\"); return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\");
} }
@Override @Override
public String getIcon(FileSystem.FileEntry entry, boolean open) { public String getIcon(FileEntry entry, boolean open) {
return open ? "default_root_folder_opened.svg" : "default_root_folder.svg"; return open ? "default_root_folder_opened.svg" : "default_root_folder.svg";
} }
}); });
@ -81,9 +82,9 @@ public abstract class BrowserIconDirectoryType {
public abstract String getId(); public abstract String getId();
public abstract boolean matches(FileSystem.FileEntry entry); public abstract boolean matches(FileEntry entry);
public abstract String getIcon(FileSystem.FileEntry entry, boolean open); public abstract String getIcon(FileEntry entry, boolean open);
public static class Simple extends BrowserIconDirectoryType { public static class Simple extends BrowserIconDirectoryType {
@ -102,16 +103,17 @@ public abstract class BrowserIconDirectoryType {
} }
@Override @Override
public boolean matches(FileSystem.FileEntry entry) { public boolean matches(FileEntry entry) {
if (entry.getKind() != FileKind.DIRECTORY) { if (entry.getKind() != FileKind.DIRECTORY) {
return false; return false;
} }
return names.contains(entry.getName()); var name = FileNames.getFileName(entry.getPath());
return names.contains(name);
} }
@Override @Override
public String getIcon(FileSystem.FileEntry entry, boolean open) { public String getIcon(FileEntry entry, boolean open) {
return open ? this.open.getIcon() : this.closed.getIcon(); return open ? this.open.getIcon() : this.closed.getIcon();
} }
} }

View file

@ -1,9 +1,10 @@
package io.xpipe.app.browser.icon; package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppResources; import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.FileNames;
import lombok.Getter; import lombok.Getter;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -60,7 +61,7 @@ public abstract class BrowserIconFileType {
public abstract String getId(); public abstract String getId();
public abstract boolean matches(FileSystem.FileEntry entry); public abstract boolean matches(FileEntry entry);
public abstract String getIcon(); public abstract String getIcon();
@ -78,14 +79,16 @@ public abstract class BrowserIconFileType {
} }
@Override @Override
public boolean matches(FileSystem.FileEntry entry) { public boolean matches(FileEntry entry) {
if (entry.getKind() == FileKind.DIRECTORY) { if (entry.getKind() == FileKind.DIRECTORY) {
return false; return false;
} }
return (entry.getExtension() != null var name = FileNames.getFileName(entry.getPath());
&& endings.contains("." + entry.getExtension().toLowerCase(Locale.ROOT))) var ext = FileNames.getExtension(entry.getPath());
|| endings.contains(entry.getName()); return (ext != null
&& endings.contains("." + ext.toLowerCase(Locale.ROOT)))
|| endings.contains(name);
} }
@Override @Override

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileEntry;
public class BrowserIcons { public class BrowserIcons {
@ -18,7 +18,7 @@ public class BrowserIcons {
return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 24); return PrettyImageHelper.ofFixedSizeSquare(type.getIcon(), 24);
} }
public static Comp<?> createIcon(FileSystem.FileEntry entry) { public static Comp<?> createIcon(FileEntry entry) {
return PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24); return PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24);
} }
} }

View file

@ -2,8 +2,8 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppImages;
import io.xpipe.app.core.AppResources; import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
public class FileIconManager { public class FileIconManager {
@ -18,7 +18,7 @@ public class FileIconManager {
} }
} }
public static synchronized String getFileIcon(FileSystem.FileEntry entry, boolean open) { public static synchronized String getFileIcon(FileEntry entry, boolean open) {
if (entry == null) { if (entry == null) {
return null; return null;
} }

View file

@ -1,5 +1,6 @@
package io.xpipe.core.process; package io.xpipe.core.process;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.NewLine; import io.xpipe.core.util.NewLine;
@ -79,7 +80,7 @@ public interface ShellDialect {
String assembleCommand(String command, Map<String, String> variables); String assembleCommand(String command, Map<String, String> variables);
Stream<FileSystem.FileEntry> listFiles(FileSystem fs, ShellControl control, String dir) throws Exception; Stream<FileEntry> listFiles(FileSystem fs, ShellControl control, String dir) throws Exception;
Stream<String> listRoots(ShellControl control) throws Exception; Stream<String> listRoots(ShellControl control) throws Exception;

View file

@ -0,0 +1,49 @@
package io.xpipe.core.store;
import lombok.NonNull;
import lombok.Setter;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.time.Instant;
@Value
@NonFinal
public class FileEntry {
FileSystem fileSystem;
Instant date;
long size;
FileInfo info;
@NonNull
FileKind kind;
@NonNull
@NonFinal
@Setter
String path;
public FileEntry(
FileSystem fileSystem, @NonNull String path, Instant date, long size, FileInfo info, @NonNull FileKind kind
) {
this.fileSystem = fileSystem;
this.kind = kind;
this.path = kind == FileKind.DIRECTORY ? new FilePath(path).toDirectory().toString() : path;
this.date = date;
this.info = info;
this.size = size;
}
public static FileEntry ofDirectory(FileSystem fileSystem, String path) {
return new FileEntry(fileSystem, path, Instant.now(), 0, null, FileKind.DIRECTORY);
}
public FileEntry resolved() {
return this;
}
public String getName() {
return FileNames.getFileName(path);
}
}

View file

@ -0,0 +1,46 @@
package io.xpipe.core.store;
import lombok.Value;
public sealed interface FileInfo permits FileInfo.Windows, FileInfo.Unix {
boolean explicitlyHidden();
boolean possiblyExecutable();
@Value
class Windows implements FileInfo {
String attributes;
@Override
public boolean explicitlyHidden() {
return attributes.contains("h");
}
@Override
public boolean possiblyExecutable() {
return true;
}
}
@Value
class Unix implements FileInfo {
String permissions;
Integer uid;
String user;
Integer gid;
String group;
@Override
public boolean explicitlyHidden() {
return false;
}
@Override
public boolean possiblyExecutable() {
return permissions.contains("x");
}
}
}

View file

@ -2,15 +2,9 @@ package io.xpipe.core.store;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.io.Closeable; import java.io.Closeable;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -62,7 +56,7 @@ public interface FileSystem extends Closeable, AutoCloseable {
try { try {
var list = new ArrayList<FileEntry>(); var list = new ArrayList<FileEntry>();
list.add(fileEntry); list.add(fileEntry);
list.addAll(listFilesRecursively(fileEntry.getPath())); list.addAll(listFilesRecursively(fileEntry.getPath().toString()));
return list.stream(); return list.stream();
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
@ -73,87 +67,4 @@ public interface FileSystem extends Closeable, AutoCloseable {
List<String> listRoots() throws Exception; List<String> listRoots() throws Exception;
@Value
@NonFinal
class FileEntry {
FileSystem fileSystem;
Instant date;
boolean hidden;
Boolean executable;
long size;
String mode;
@NonNull
FileKind kind;
@NonNull
@NonFinal
String path;
@NonFinal
String extension;
@NonFinal
String name;
public FileEntry(
FileSystem fileSystem,
@NonNull String path,
Instant date,
boolean hidden,
Boolean executable,
long size,
String mode,
@NonNull FileKind kind) {
this.fileSystem = fileSystem;
this.mode = mode;
this.kind = kind;
this.path = kind == FileKind.DIRECTORY ? FileNames.toDirectory(path) : path;
this.extension = FileNames.getExtension(path);
this.name = FileNames.getFileName(path);
this.date = date;
this.hidden = hidden;
this.executable = executable;
this.size = size;
}
public static FileEntry ofDirectory(FileSystem fileSystem, String path) {
return new FileEntry(fileSystem, path, Instant.now(), true, false, 0, null, FileKind.DIRECTORY);
}
public void setPath(String path) {
this.path = path;
this.extension = FileNames.getExtension(path);
this.name = FileNames.getFileName(path);
}
public FileEntry resolved() {
return this;
}
}
@Value
@EqualsAndHashCode(callSuper = true)
class LinkFileEntry extends FileEntry {
@NonNull
FileEntry target;
public LinkFileEntry(
@NonNull FileSystem fileSystem,
@NonNull String path,
Instant date,
boolean hidden,
Boolean executable,
long size,
String mode,
@NonNull FileEntry target) {
super(fileSystem, path, date, hidden, executable, size, mode, FileKind.LINK);
this.target = target;
}
public FileEntry resolved() {
return target;
}
}
} }

View file

@ -0,0 +1,27 @@
package io.xpipe.core.store;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value;
import java.time.Instant;
@Value
@EqualsAndHashCode(callSuper = true)
public class LinkFileEntry extends FileEntry {
@NonNull
FileEntry target;
public LinkFileEntry(
FileSystem fileSystem, @NonNull String path, Instant date, long size, @NonNull FileInfo info,
@NonNull FileEntry target
) {
super(fileSystem, path, date, size, info, FileKind.LINK);
this.target = target;
}
public FileEntry resolved() {
return target;
}
}

View file

@ -5,6 +5,7 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.BrowserIconFileType; import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.core.store.FileNames;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -34,6 +35,6 @@ public class JavapAction extends ToFileCommandAction implements FileTypeAction,
@Override @Override
protected String createCommand(OpenFileSystemModel model, BrowserEntry entry) { protected String createCommand(OpenFileSystemModel model, BrowserEntry entry) {
return "javap -c -p " + entry.getOptionallyQuotedFileName(); return "javap -c -p " + FileNames.quoteIfNecessary(entry.getRawFileEntry().getPath());
} }
} }

View file

@ -7,8 +7,8 @@ import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialects; import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.Node; import javafx.scene.Node;
@ -20,12 +20,12 @@ import java.util.stream.Stream;
public class RunAction extends MultiExecuteAction { public class RunAction extends MultiExecuteAction {
private boolean isExecutable(FileSystem.FileEntry e) { private boolean isExecutable(FileEntry e) {
if (e.getKind() != FileKind.FILE) { if (e.getKind() != FileKind.FILE) {
return false; return false;
} }
if (e.getExecutable() != null && e.getExecutable()) { if (e.getInfo() != null && e.getInfo().possiblyExecutable()) {
return true; return true;
} }

View file

@ -25,7 +25,7 @@ public class UnzipAction extends ExecuteApplicationAction implements FileTypeAct
@Override @Override
protected String createCommand(OpenFileSystemModel model, BrowserEntry entry) { protected String createCommand(OpenFileSystemModel model, BrowserEntry entry) {
return "unzip -o " + entry.getOptionallyQuotedFileName() + " -d " return "unzip -o " + FileNames.quoteIfNecessary(entry.getRawFileEntry().getPath()) + " -d "
+ FileNames.quoteIfNecessary(FileNames.getBaseName(entry.getFileName())); + FileNames.quoteIfNecessary(FileNames.getBaseName(entry.getFileName()));
} }

View file

@ -440,6 +440,7 @@ sshConfiguration=SSH-konfiguration
size=Størrelse size=Størrelse
attributes=Attributter attributes=Attributter
modified=Ændret modified=Ændret
owner=Ejer
isOnlySupported=understøttes kun med en licens isOnlySupported=understøttes kun med en licens
areOnlySupported=understøttes kun med en licens areOnlySupported=understøttes kun med en licens
updateReadyTitle=Opdatering til $VERSION$ klar updateReadyTitle=Opdatering til $VERSION$ klar

View file

@ -436,6 +436,7 @@ sshConfiguration=SSH-Konfiguration
size=Größe size=Größe
attributes=Attribute attributes=Attribute
modified=Geändert modified=Geändert
owner=Eigentümer
isOnlySupported=wird nur mit einer Lizenz unterstützt isOnlySupported=wird nur mit einer Lizenz unterstützt
areOnlySupported=werden nur mit einer Lizenz unterstützt areOnlySupported=werden nur mit einer Lizenz unterstützt
updateReadyTitle=Update auf $VERSION$ bereit updateReadyTitle=Update auf $VERSION$ bereit

View file

@ -439,6 +439,7 @@ size=Size
attributes=Attributes attributes=Attributes
#context: title, last modified date #context: title, last modified date
modified=Modified modified=Modified
owner=Owner
isOnlySupported=is only supported with a license isOnlySupported=is only supported with a license
areOnlySupported=are only supported with a license areOnlySupported=are only supported with a license
updateReadyTitle=Update to $VERSION$ ready updateReadyTitle=Update to $VERSION$ ready

View file

@ -424,6 +424,7 @@ sshConfiguration=Configuración SSH
size=Tamaño size=Tamaño
attributes=Atributos attributes=Atributos
modified=Modificado modified=Modificado
owner=Propietario
isOnlySupported=sólo es compatible con una licencia isOnlySupported=sólo es compatible con una licencia
areOnlySupported=sólo se admiten con licencia areOnlySupported=sólo se admiten con licencia
updateReadyTitle=Actualiza a $VERSION$ ready updateReadyTitle=Actualiza a $VERSION$ ready

View file

@ -424,6 +424,7 @@ sshConfiguration=Configuration SSH
size=Taille size=Taille
attributes=Attributs attributes=Attributs
modified=Modifié modified=Modifié
owner=Propriétaire
isOnlySupported=n'est pris en charge qu'avec une licence isOnlySupported=n'est pris en charge qu'avec une licence
areOnlySupported=ne sont prises en charge qu'avec une licence areOnlySupported=ne sont prises en charge qu'avec une licence
updateReadyTitle=Mise à jour de $VERSION$ ready updateReadyTitle=Mise à jour de $VERSION$ ready

View file

@ -424,6 +424,7 @@ sshConfiguration=Configurazione SSH
size=Dimensione size=Dimensione
attributes=Attributi attributes=Attributi
modified=Modificato modified=Modificato
owner=Proprietario
isOnlySupported=è supportato solo con una licenza isOnlySupported=è supportato solo con una licenza
areOnlySupported=sono supportati solo con una licenza areOnlySupported=sono supportati solo con una licenza
updateReadyTitle=Aggiornamento a $VERSION$ ready updateReadyTitle=Aggiornamento a $VERSION$ ready

View file

@ -424,6 +424,7 @@ sshConfiguration=SSHの設定
size=サイズ size=サイズ
attributes=属性 attributes=属性
modified=変更された modified=変更された
owner=所有者
isOnlySupported=がサポートされているのは、ライセンス isOnlySupported=がサポートされているのは、ライセンス
areOnlySupported=がサポートされるのはライセンスが必要である areOnlySupported=がサポートされるのはライセンスが必要である
updateReadyTitle=$VERSION$ に更新 updateReadyTitle=$VERSION$ に更新

View file

@ -424,6 +424,7 @@ sshConfiguration=SSH-configuratie
size=Grootte size=Grootte
attributes=Attributen attributes=Attributen
modified=Gewijzigd modified=Gewijzigd
owner=Eigenaar
isOnlySupported=wordt alleen ondersteund met een licentie isOnlySupported=wordt alleen ondersteund met een licentie
areOnlySupported=worden alleen ondersteund met een licentie areOnlySupported=worden alleen ondersteund met een licentie
updateReadyTitle=Bijwerken naar $VERSION$ klaar updateReadyTitle=Bijwerken naar $VERSION$ klaar

View file

@ -424,6 +424,7 @@ sshConfiguration=Configuração SSH
size=Tamanho size=Tamanho
attributes=Atribui attributes=Atribui
modified=Modificado modified=Modificado
owner=Proprietário
isOnlySupported=só é suportado com uma licença isOnlySupported=só é suportado com uma licença
areOnlySupported=só são suportados com uma licença areOnlySupported=só são suportados com uma licença
updateReadyTitle=Actualiza para $VERSION$ ready updateReadyTitle=Actualiza para $VERSION$ ready

View file

@ -424,6 +424,7 @@ sshConfiguration=Конфигурация SSH
size=Размер size=Размер
attributes=Атрибуты attributes=Атрибуты
modified=Изменено modified=Изменено
owner=Владелец
isOnlySupported=поддерживается только при наличии лицензии isOnlySupported=поддерживается только при наличии лицензии
areOnlySupported=поддерживаются только при наличии лицензии areOnlySupported=поддерживаются только при наличии лицензии
updateReadyTitle=Обновление на $VERSION$ готово updateReadyTitle=Обновление на $VERSION$ готово

View file

@ -425,6 +425,7 @@ sshConfiguration=SSH Yapılandırması
size=Boyut size=Boyut
attributes=Nitelikler attributes=Nitelikler
modified=Değiştirilmiş modified=Değiştirilmiş
owner=Sahibi
isOnlySupported=yalnızca bir lisans ile desteklenir isOnlySupported=yalnızca bir lisans ile desteklenir
areOnlySupported=yalnızca bir lisans ile desteklenir areOnlySupported=yalnızca bir lisans ile desteklenir
updateReadyTitle=$VERSION$ için güncelleme hazır updateReadyTitle=$VERSION$ için güncelleme hazır

View file

@ -424,6 +424,7 @@ sshConfiguration=SSH 配置
size=大小 size=大小
attributes=属性 attributes=属性
modified=已修改 modified=已修改
owner=所有者
isOnlySupported=只有获得许可后才支持 isOnlySupported=只有获得许可后才支持
areOnlySupported=只有获得许可后才支持 areOnlySupported=只有获得许可后才支持
updateReadyTitle=更新至$VERSION$ ready updateReadyTitle=更新至$VERSION$ ready