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.util.ThreadHelper;
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 javafx.beans.property.Property;
@ -69,7 +69,7 @@ public class BrowserClipboard {
@SneakyThrows
public static ClipboardContent startDrag(
FileSystem.FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) {
FileEntry base, List<BrowserEntry> selected, BrowserFileTransferMode mode) {
if (selected.isEmpty()) {
return null;
}
@ -82,7 +82,7 @@ public class BrowserClipboard {
}
@SneakyThrows
public static void startCopy(FileSystem.FileEntry base, List<BrowserEntry> selected) {
public static void startCopy(FileEntry base, List<BrowserEntry> selected) {
if (selected.isEmpty()) {
currentCopyClipboard.setValue(null);
return;
@ -118,7 +118,7 @@ public class BrowserClipboard {
@Value
public static class Instance {
UUID uuid;
FileSystem.FileEntry baseDirectory;
FileEntry baseDirectory;
List<BrowserEntry> entries;
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.FileBridge;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.store.FileEntry;
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) {
public static void openWithAnyApplication(OpenFileSystemModel model, FileEntry entry) {
var file = entry.getPath();
var key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
@ -33,7 +33,7 @@ public class BrowserFileOpener {
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 key = entry.getPath().hashCode() + entry.getFileSystem().hashCode();
FileBridge.get()
@ -54,7 +54,7 @@ public class BrowserFileOpener {
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();
if (editor == null) {
return;

View file

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

View file

@ -7,7 +7,7 @@ import java.util.List;
public class BrowserActionFormatter {
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) {

View file

@ -2,9 +2,9 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
@ -45,7 +45,7 @@ public class BrowserAlerts {
.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)) {
return true;
}
@ -61,7 +61,7 @@ public class BrowserAlerts {
.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)) {
return true;
}
@ -77,7 +77,7 @@ public class BrowserAlerts {
.orElse(false);
}
private static String getSelectedElementsString(List<FileSystem.FileEntry> source) {
private static String getSelectedElementsString(List<FileEntry> source) {
var namesHeader = AppI18n.get("selectedElements");
var names = namesHeader + "\n"
+ 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.BrowserIconFileType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import lombok.Getter;
@Getter
public class BrowserEntry {
private final BrowserFileListModel model;
private final FileSystem.FileEntry rawFileEntry;
private final FileEntry rawFileEntry;
private final BrowserIconFileType fileType;
private final BrowserIconDirectoryType directoryType;
public BrowserEntry(FileSystem.FileEntry rawFileEntry, BrowserFileListModel model) {
public BrowserEntry(FileEntry rawFileEntry, BrowserFileListModel model) {
this.rawFileEntry = rawFileEntry;
this.model = model;
this.fileType = fileType(rawFileEntry);
this.directoryType = directoryType(rawFileEntry);
}
private static BrowserIconFileType fileType(FileSystem.FileEntry rawFileEntry) {
private static BrowserIconFileType fileType(FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
@ -42,7 +41,7 @@ public class BrowserEntry {
return null;
}
private static BrowserIconDirectoryType directoryType(FileSystem.FileEntry rawFileEntry) {
private static BrowserIconDirectoryType directoryType(FileEntry rawFileEntry) {
if (rawFileEntry == null) {
return null;
}
@ -74,11 +73,6 @@ public class BrowserEntry {
}
public String getFileName() {
return getRawFileEntry().getName();
}
public String getOptionallyQuotedFileName() {
var n = getFileName();
return FileNames.quoteIfNecessary(n);
return FileNames.getFileName(getRawFileEntry().getPath());
}
}

View file

@ -1,5 +1,7 @@
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.comp.base.LazyTextFieldComp;
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.util.*;
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.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@ -32,9 +34,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
@ -58,6 +57,7 @@ public final class BrowserFileListComp extends SimpleComp {
private final BrowserFileListModel fileList;
private final StringProperty typedSelection = new SimpleStringProperty("");
private final DoubleProperty ownerWidth = new SimpleDoubleProperty();
public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList;
@ -103,13 +103,24 @@ public final class BrowserFileListComp extends SimpleComp {
var modeCol = new TableColumn<BrowserEntry, String>();
modeCol.textProperty().bind(AppI18n.observable("attributes"));
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.setResizable(false);
modeCol.setPrefWidth(120);
modeCol.setSortable(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>();
table.setAccessibleText("Directory contents");
table.setPlaceholder(new Region());
@ -121,18 +132,39 @@ public final class BrowserFileListComp extends SimpleComp {
fileList.setComparator(table.getComparator());
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.widthProperty().subscribe((newValue) -> {
ownerCol.setVisible(newValue.doubleValue() > 1000);
});
prepareTableSelectionModel(table);
prepareTableShortcuts(table);
prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol);
prepareTableChanges(table, mtimeCol, modeCol, ownerCol);
prepareTypedSelectionModel(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) {
AtomicReference<Instant> lastFail = new AtomicReference<>();
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
@ -369,8 +401,9 @@ public final class BrowserFileListComp extends SimpleComp {
private void prepareTableChanges(
TableView<BrowserEntry> table,
TableColumn<BrowserEntry, Instant> mtimeCol,
TableColumn<BrowserEntry, String> modeCol) {
var lastDir = new SimpleObjectProperty<FileSystem.FileEntry>();
TableColumn<BrowserEntry, String> modeCol,
TableColumn<BrowserEntry, String> ownerCol) {
var lastDir = new SimpleObjectProperty<FileEntry>();
Runnable updateHandler = () -> {
Platform.runLater(() -> {
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) {
var shell = fileList.getFileSystemModel()
.getFileSystem()
.getShell()
.orElseThrow();
var hasAttributes = !OsType.WINDOWS.equals(shell.getOsType());
if (!hasAttributes) {
var notWindows = !OsType.WINDOWS.equals(shell.getOsType());
if (!notWindows) {
table.getColumns().remove(modeCol);
table.getColumns().remove(ownerCol);
} else {
if (!table.getColumns().contains(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> {
@Override
@ -648,7 +708,7 @@ public final class BrowserFileListComp extends SimpleComp {
.getPath()
: getTableRow().getItem().getFileName();
var fileName = normalName;
var hidden = getTableRow().getItem().getRawFileEntry().isHidden() || fileName.startsWith(".");
var hidden = getTableRow().getItem().getRawFileEntry().getInfo().explicitlyHidden() || fileName.startsWith(".");
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
// 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.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import javafx.beans.property.Property;
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) {
var l = s.filter(entry -> entry != null)
.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.augment.GrowAugment;
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.geometry.Pos;
@ -26,12 +26,12 @@ import java.util.function.Function;
public class BrowserFileOverviewComp extends SimpleComp {
OpenFileSystemModel model;
ObservableList<FileSystem.FileEntry> list;
ObservableList<FileEntry> list;
boolean grow;
@Override
protected Region createSimple() {
Function<FileSystem.FileEntry, Comp<?>> factory = entry -> {
Function<FileEntry, Comp<?>> factory = entry -> {
return Comp.of(() -> {
var icon = BrowserIcons.createIcon(entry);
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.issue.ErrorEvent;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.*;
import java.io.*;
import java.nio.file.Files;
@ -18,8 +15,8 @@ import java.util.function.Consumer;
public class BrowserFileTransferOperation {
private final FileSystem.FileEntry target;
private final List<FileSystem.FileEntry> files;
private final FileEntry target;
private final List<FileEntry> files;
private final BrowserFileTransferMode transferMode;
private final boolean checkConflicts;
private final Consumer<BrowserTransferProgress> progress;
@ -27,8 +24,8 @@ public class BrowserFileTransferOperation {
BrowserAlerts.FileConflictChoice lastConflictChoice;
public BrowserFileTransferOperation(
FileSystem.FileEntry target,
List<FileSystem.FileEntry> files,
FileEntry target,
List<FileEntry> files,
BrowserFileTransferMode transferMode,
boolean checkConflicts,
Consumer<BrowserTransferProgress> progress) {
@ -40,7 +37,7 @@ public class BrowserFileTransferOperation {
}
public static BrowserFileTransferOperation ofLocal(
FileSystem.FileEntry target,
FileEntry target,
List<Path> files,
BrowserFileTransferMode transferMode,
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
if (source.getPath().equals(target.getPath())) {
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) {
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
if (source.getFileSystem().equals(target.getFileSystem())
@ -182,8 +179,8 @@ public class BrowserFileTransferOperation {
flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileSystem.FileEntry fileEntry : list) {
List<FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileEntry fileEntry : list) {
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
@ -225,7 +222,7 @@ public class BrowserFileTransferOperation {
}
private void transfer(
FileSystem.FileEntry sourceFile,
FileEntry sourceFile,
String targetFile,
AtomicLong transferred,
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());
}
private static final int DEFAULT_BUFFER_SIZE = 1024;
private void transferFile(
FileSystem.FileEntry sourceFile,
FileEntry sourceFile,
InputStream inputStream,
OutputStream outputStream,
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.InputHelper;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.application.Platform;
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 {
List<FileSystem.FileEntry> list = new ArrayList<>();
List<FileEntry> list = new ArrayList<>();
model.withFiles(entry.getRawFileEntry().resolved().getPath(), newFiles -> {
try (var s = newFiles) {
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.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
@ -125,19 +126,17 @@ public class FileSystemHelper {
}
}
public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
return new FileSystem.FileEntry(
public static FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
return new FileEntry(
fileSystem,
file,
Instant.now(),
false,
false,
fileSystem.getFileSize(file),
null,
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()) {
return;
}

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser.file;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
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) {
throw new IllegalStateException();
}
return new FileSystem.FileEntry(
return new FileEntry(
localFileSystem.open(),
file.toString(),
Files.getLastModifiedTime(file).toInstant(),
Files.isHidden(file),
Files.isExecutable(file),
Files.size(file),
null,
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);

View file

@ -1,16 +1,23 @@
package io.xpipe.app.browser.fs;
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.ShellDialect;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
@Getter
public class OpenFileSystemCache extends ShellControlCache {
private final OpenFileSystemModel model;
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 {
super(model.getFileSystem().getShell().orElseThrow());
@ -20,6 +27,42 @@ public class OpenFileSystemCache extends ShellControlCache {
ShellDialect d = sc.getShellDialect();
// If there is no id command, we should still be fine with just assuming 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() {

View file

@ -166,7 +166,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
cdSyncWithoutCheck(currentPath.get());
}
public FileSystem.FileEntry getCurrentParentDirectory() {
public FileEntry getCurrentParentDirectory() {
var current = getCurrentDirectory();
if (current == null) {
return null;
@ -177,10 +177,10 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
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) {
return null;
}
@ -189,7 +189,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
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) {
@ -305,7 +305,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
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 {
BooleanScope.executeExclusive(busy, () -> {
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(() -> {
BooleanScope.executeExclusive(busy, () -> {
if (fileSystem == null) {
@ -358,7 +358,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
}
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
if (files.isEmpty()) {
return;

View file

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

View file

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

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.FileEntry;
public class BrowserIcons {
@ -18,7 +18,7 @@ public class BrowserIcons {
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);
}
}

View file

@ -2,8 +2,8 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.core.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
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) {
return null;
}

View file

@ -1,5 +1,6 @@
package io.xpipe.core.process;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.util.NewLine;
@ -79,7 +80,7 @@ public interface ShellDialect {
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;

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 lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value;
import lombok.experimental.NonFinal;
import java.io.Closeable;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@ -62,7 +56,7 @@ public interface FileSystem extends Closeable, AutoCloseable {
try {
var list = new ArrayList<FileEntry>();
list.add(fileEntry);
list.addAll(listFilesRecursively(fileEntry.getPath()));
list.addAll(listFilesRecursively(fileEntry.getPath().toString()));
return list.stream();
} catch (Exception e) {
throw new RuntimeException(e);
@ -73,87 +67,4 @@ public interface FileSystem extends Closeable, AutoCloseable {
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.icon.BrowserIconFileType;
import io.xpipe.core.store.FileNames;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@ -34,6 +35,6 @@ public class JavapAction extends ToFileCommandAction implements FileTypeAction,
@Override
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.ShellControl;
import io.xpipe.core.process.ShellDialects;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileSystem;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
@ -20,12 +20,12 @@ import java.util.stream.Stream;
public class RunAction extends MultiExecuteAction {
private boolean isExecutable(FileSystem.FileEntry e) {
private boolean isExecutable(FileEntry e) {
if (e.getKind() != FileKind.FILE) {
return false;
}
if (e.getExecutable() != null && e.getExecutable()) {
if (e.getInfo() != null && e.getInfo().possiblyExecutable()) {
return true;
}

View file

@ -25,7 +25,7 @@ public class UnzipAction extends ExecuteApplicationAction implements FileTypeAct
@Override
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()));
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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