diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java index 2e4b2bef3..672ab588d 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserClipboard.java @@ -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 selected, BrowserFileTransferMode mode) { + FileEntry base, List selected, BrowserFileTransferMode mode) { if (selected.isEmpty()) { return null; } @@ -82,7 +82,7 @@ public class BrowserClipboard { } @SneakyThrows - public static void startCopy(FileSystem.FileEntry base, List selected) { + public static void startCopy(FileEntry base, List 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 entries; BrowserFileTransferMode mode; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java b/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java index fb500e77f..a25524f50 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFileOpener.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java index 6931e634a..d6165f71c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java @@ -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.observableArrayList(); + var commonPlatform = FXCollections.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); diff --git a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java index 39a8cddb0..fd7b02903 100644 --- a/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java +++ b/app/src/main/java/io/xpipe/app/browser/action/BrowserActionFormatter.java @@ -7,7 +7,7 @@ import java.util.List; public class BrowserActionFormatter { public static String filesArgument(List 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) { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java index 3ea4510b9..80e71cdff 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserAlerts.java @@ -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 source, FileSystem.FileEntry target) { + public static boolean showMoveAlert(List 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 source) { + public static boolean showDeleteAlert(List 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 source) { + private static String getSelectedElementsString(List source) { var namesHeader = AppI18n.get("selectedElements"); var names = namesHeader + "\n" + source.stream() diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java index d8e617992..c386c5761 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java @@ -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()); } } diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java index f4b19e9b8..2d957220e 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListComp.java @@ -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(); 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(); + 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(); 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 table) { AtomicReference lastFail = new AtomicReference<>(); table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { @@ -369,8 +401,9 @@ public final class BrowserFileListComp extends SimpleComp { private void prepareTableChanges( TableView table, TableColumn mtimeCol, - TableColumn modeCol) { - var lastDir = new SimpleObjectProperty(); + TableColumn modeCol, + TableColumn ownerCol) { + var lastDir = new SimpleObjectProperty(); 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 { + + 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 { @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 diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java index 21f3141b1..8495b91ba 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileListModel.java @@ -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 newFiles) { + public void setAll(Stream newFiles) { try (var s = newFiles) { var l = s.filter(entry -> entry != null) .map(entry -> new BrowserEntry(entry, this)) diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java index 2aaca8796..5fa28b999 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileOverviewComp.java @@ -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 list; + ObservableList list; boolean grow; @Override protected Region createSimple() { - Function> factory = entry -> { + Function> factory = entry -> { return Comp.of(() -> { var icon = BrowserIcons.createIcon(entry); var graphic = new HorizontalComp(List.of( diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java index 640d7c868..357c69775 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserFileTransferOperation.java @@ -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 files; + private final FileEntry target; + private final List files; private final BrowserFileTransferMode transferMode; private final boolean checkConflicts; private final Consumer progress; @@ -27,8 +24,8 @@ public class BrowserFileTransferOperation { BrowserAlerts.FileConflictChoice lastConflictChoice; public BrowserFileTransferOperation( - FileSystem.FileEntry target, - List files, + FileEntry target, + List files, BrowserFileTransferMode transferMode, boolean checkConflicts, Consumer progress) { @@ -40,7 +37,7 @@ public class BrowserFileTransferOperation { } public static BrowserFileTransferOperation ofLocal( - FileSystem.FileEntry target, + FileEntry target, List 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(); + var flatFiles = new LinkedHashMap(); // 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 list = source.getFileSystem().listFilesRecursively(source.getPath()); - for (FileSystem.FileEntry fileEntry : list) { + List 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, diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java index a1c0d3ba1..9052f654f 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserQuickAccessContextMenu.java @@ -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 updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception { - List list = new ArrayList<>(); + List list = new ArrayList<>(); model.withFiles(entry.getRawFileEntry().resolved().getPath(), newFiles -> { try (var s = newFiles) { var l = s.map(fileEntry -> fileEntry.resolved()).toList(); diff --git a/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java b/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java index ee6931a43..5a76bc1ff 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java +++ b/app/src/main/java/io/xpipe/app/browser/file/FileSystemHelper.java @@ -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 files) { + public static void delete(List files) { if (files.isEmpty()) { return; } diff --git a/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java b/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java index c3b9ccac4..1c15ba45a 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java +++ b/app/src/main/java/io/xpipe/app/browser/file/LocalFileSystem.java @@ -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); diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemCache.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemCache.java index 6553057ab..135e518fe 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemCache.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemCache.java @@ -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 users = new HashMap<>(); + private final Map 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() { diff --git a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java index 824557bb2..c425c115f 100644 --- a/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java +++ b/app/src/main/java/io/xpipe/app/browser/fs/OpenFileSystemModel.java @@ -166,7 +166,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab, Exception> consumer) + public void withFiles(String dir, FailableConsumer, Exception> consumer) throws Exception { BooleanScope.executeExclusive(busy, () -> { if (dir != null) { @@ -341,7 +341,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab files) { + public void dropLocalFilesIntoAsync(FileEntry entry, List files) { ThreadHelper.runFailableAsync(() -> { BooleanScope.executeExclusive(busy, () -> { if (fileSystem == null) { @@ -358,7 +358,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab files, BrowserFileTransferMode mode) { + FileEntry target, List files, BrowserFileTransferMode mode) { // We don't have to do anything in this case if (files.isEmpty()) { return; diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java index 9a3a45ae9..a3b28796e 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconDirectoryType.java @@ -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(); } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java index afff37e3b..9ec053373 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIconFileType.java @@ -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 diff --git a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java index 0aa842ee4..e156867ea 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/BrowserIcons.java @@ -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); } } diff --git a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java index 175bb451c..4df214cd7 100644 --- a/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java +++ b/app/src/main/java/io/xpipe/app/browser/icon/FileIconManager.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index e4f474ef3..34c782f7f 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -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 variables); - Stream listFiles(FileSystem fs, ShellControl control, String dir) throws Exception; + Stream listFiles(FileSystem fs, ShellControl control, String dir) throws Exception; Stream listRoots(ShellControl control) throws Exception; diff --git a/core/src/main/java/io/xpipe/core/store/FileEntry.java b/core/src/main/java/io/xpipe/core/store/FileEntry.java new file mode 100644 index 000000000..33c563f69 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/FileEntry.java @@ -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); + } +} diff --git a/core/src/main/java/io/xpipe/core/store/FileInfo.java b/core/src/main/java/io/xpipe/core/store/FileInfo.java new file mode 100644 index 000000000..3aa2408f8 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/FileInfo.java @@ -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"); + } + } +} diff --git a/core/src/main/java/io/xpipe/core/store/FileSystem.java b/core/src/main/java/io/xpipe/core/store/FileSystem.java index eb46022b6..1f27b744f 100644 --- a/core/src/main/java/io/xpipe/core/store/FileSystem.java +++ b/core/src/main/java/io/xpipe/core/store/FileSystem.java @@ -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(); 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 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; - } - } } diff --git a/core/src/main/java/io/xpipe/core/store/LinkFileEntry.java b/core/src/main/java/io/xpipe/core/store/LinkFileEntry.java new file mode 100644 index 000000000..a92f15527 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/LinkFileEntry.java @@ -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; + } +} diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java index 076157ebe..2c4216c6e 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/JavapAction.java @@ -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()); } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java index 505786ff0..c9fc57975 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/RunAction.java @@ -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; } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java b/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java index c4f34ab36..2dfa938f9 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/browser/UnzipAction.java @@ -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())); } diff --git a/lang/app/strings/translations_da.properties b/lang/app/strings/translations_da.properties index e113e1912..ee84e7848 100644 --- a/lang/app/strings/translations_da.properties +++ b/lang/app/strings/translations_da.properties @@ -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 diff --git a/lang/app/strings/translations_de.properties b/lang/app/strings/translations_de.properties index d279ad9d3..0835708c7 100644 --- a/lang/app/strings/translations_de.properties +++ b/lang/app/strings/translations_de.properties @@ -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 diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 1239c40a4..ae24c4eb5 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -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 diff --git a/lang/app/strings/translations_es.properties b/lang/app/strings/translations_es.properties index 1c508b1e9..b7b47d561 100644 --- a/lang/app/strings/translations_es.properties +++ b/lang/app/strings/translations_es.properties @@ -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 diff --git a/lang/app/strings/translations_fr.properties b/lang/app/strings/translations_fr.properties index 77a8394c4..772be96f6 100644 --- a/lang/app/strings/translations_fr.properties +++ b/lang/app/strings/translations_fr.properties @@ -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 diff --git a/lang/app/strings/translations_it.properties b/lang/app/strings/translations_it.properties index d76aab2ae..31e4403a2 100644 --- a/lang/app/strings/translations_it.properties +++ b/lang/app/strings/translations_it.properties @@ -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 diff --git a/lang/app/strings/translations_ja.properties b/lang/app/strings/translations_ja.properties index c123be776..51f13940a 100644 --- a/lang/app/strings/translations_ja.properties +++ b/lang/app/strings/translations_ja.properties @@ -424,6 +424,7 @@ sshConfiguration=SSHの設定 size=サイズ attributes=属性 modified=変更された +owner=所有者 isOnlySupported=がサポートされているのは、ライセンス areOnlySupported=がサポートされるのはライセンスが必要である updateReadyTitle=$VERSION$ に更新 diff --git a/lang/app/strings/translations_nl.properties b/lang/app/strings/translations_nl.properties index 7f9056444..e0fabc829 100644 --- a/lang/app/strings/translations_nl.properties +++ b/lang/app/strings/translations_nl.properties @@ -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 diff --git a/lang/app/strings/translations_pt.properties b/lang/app/strings/translations_pt.properties index 95837881e..71b707f92 100644 --- a/lang/app/strings/translations_pt.properties +++ b/lang/app/strings/translations_pt.properties @@ -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 diff --git a/lang/app/strings/translations_ru.properties b/lang/app/strings/translations_ru.properties index 6c0693d1c..cd1ec6810 100644 --- a/lang/app/strings/translations_ru.properties +++ b/lang/app/strings/translations_ru.properties @@ -424,6 +424,7 @@ sshConfiguration=Конфигурация SSH size=Размер attributes=Атрибуты modified=Изменено +owner=Владелец isOnlySupported=поддерживается только при наличии лицензии areOnlySupported=поддерживаются только при наличии лицензии updateReadyTitle=Обновление на $VERSION$ готово diff --git a/lang/app/strings/translations_tr.properties b/lang/app/strings/translations_tr.properties index cc4197c2a..945cf4cc7 100644 --- a/lang/app/strings/translations_tr.properties +++ b/lang/app/strings/translations_tr.properties @@ -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 diff --git a/lang/app/strings/translations_zh.properties b/lang/app/strings/translations_zh.properties index 93de3ea8c..d78dadb59 100644 --- a/lang/app/strings/translations_zh.properties +++ b/lang/app/strings/translations_zh.properties @@ -424,6 +424,7 @@ sshConfiguration=SSH 配置 size=大小 attributes=属性 modified=已修改 +owner=所有者 isOnlySupported=只有获得许可后才支持 areOnlySupported=只有获得许可后才支持 updateReadyTitle=更新至$VERSION$ ready