From e52f3fe7870487222dc58478c91f2221d33a3d60 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Mar 2023 11:08:02 +0100 Subject: [PATCH 01/33] Define versioning support in protocols. --- .../cyberduck/core/azure/AzureProtocol.java | 5 ++++ .../java/ch/cyberduck/core/b2/B2Protocol.java | 5 ++++ .../ch/cyberduck/core/box/BoxProtocol.java | 5 ++++ .../cyberduck/core/brick/BrickProtocol.java | 5 ++++ .../ch/cyberduck/core/AbstractProtocol.java | 5 ++++ .../main/java/ch/cyberduck/core/Profile.java | 5 ++++ .../main/java/ch/cyberduck/core/Protocol.java | 23 +++++++++++++++++++ .../cyberduck/core/ctera/CteraProtocol.java | 5 ++++ .../ch/cyberduck/core/sds/SDSProtocol.java | 5 ++++ .../core/dropbox/DropboxProtocol.java | 5 ++++ .../ch/cyberduck/core/eue/EueProtocol.java | 5 ++++ .../ch/cyberduck/core/ftp/FTPProtocol.java | 6 +++++ .../ch/cyberduck/core/ftp/FTPTLSProtocol.java | 6 +++++ .../core/googledrive/DriveProtocol.java | 5 ++++ .../googlestorage/GoogleStorageProtocol.java | 5 ++++ .../cyberduck/core/hubic/HubicProtocol.java | 5 ++++ .../cyberduck/core/irods/IRODSProtocol.java | 5 ++++ .../cyberduck/core/manta/MantaProtocol.java | 5 ++++ .../core/nextcloud/NextcloudProtocol.java | 5 ++++ .../ch/cyberduck/core/nio/LocalProtocol.java | 5 ++++ .../core/onedrive/GraphProtocol.java | 5 ++++ .../core/onedrive/OneDriveProtocol.java | 5 ++++ .../core/onedrive/SharepointProtocol.java | 5 ++++ .../core/onedrive/SharepointSiteProtocol.java | 5 ++++ .../core/openstack/SwiftProtocol.java | 5 ++++ .../core/owncloud/OwncloudProtocol.java | 5 ++++ .../java/ch/cyberduck/core/s3/S3Protocol.java | 5 ++++ .../core/spectra/SpectraProtocol.java | 5 ++++ .../ch/cyberduck/core/sftp/SFTPProtocol.java | 5 ++++ .../ch/cyberduck/core/dav/DAVProtocol.java | 5 ++++ .../ch/cyberduck/core/dav/DAVSSLProtocol.java | 5 ++++ 31 files changed, 175 insertions(+) diff --git a/azure/src/main/java/ch/cyberduck/core/azure/AzureProtocol.java b/azure/src/main/java/ch/cyberduck/core/azure/AzureProtocol.java index 9dfbd19b869..06f463bad7e 100644 --- a/azure/src/main/java/ch/cyberduck/core/azure/AzureProtocol.java +++ b/azure/src/main/java/ch/cyberduck/core/azure/AzureProtocol.java @@ -94,6 +94,11 @@ public Comparator getListComparator() { return new DefaultLexicographicOrderComparator(); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/backblaze/src/main/java/ch/cyberduck/core/b2/B2Protocol.java b/backblaze/src/main/java/ch/cyberduck/core/b2/B2Protocol.java index 78a6284ad46..127511ec36c 100644 --- a/backblaze/src/main/java/ch/cyberduck/core/b2/B2Protocol.java +++ b/backblaze/src/main/java/ch/cyberduck/core/b2/B2Protocol.java @@ -82,6 +82,11 @@ public Comparator getListComparator() { return new DefaultLexicographicOrderComparator(); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/box/src/main/java/ch/cyberduck/core/box/BoxProtocol.java b/box/src/main/java/ch/cyberduck/core/box/BoxProtocol.java index ab72c75e26a..21243e8fbf9 100644 --- a/box/src/main/java/ch/cyberduck/core/box/BoxProtocol.java +++ b/box/src/main/java/ch/cyberduck/core/box/BoxProtocol.java @@ -63,6 +63,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.explicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override public T getFeature(final Class type) { if(type == ComparisonService.class) { diff --git a/brick/src/main/java/ch/cyberduck/core/brick/BrickProtocol.java b/brick/src/main/java/ch/cyberduck/core/brick/BrickProtocol.java index f934432d193..8c52ece3e4e 100644 --- a/brick/src/main/java/ch/cyberduck/core/brick/BrickProtocol.java +++ b/brick/src/main/java/ch/cyberduck/core/brick/BrickProtocol.java @@ -65,6 +65,11 @@ public boolean validate(final Credentials credentials, final LoginOptions option return true; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java b/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java index 92e8e9c246a..efd880df45f 100644 --- a/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java +++ b/core/src/main/java/ch/cyberduck/core/AbstractProtocol.java @@ -395,6 +395,11 @@ public Comparator getListComparator() { return new NullComparator<>(); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } + @Override public Map getProperties() { return Collections.emptyMap(); diff --git a/core/src/main/java/ch/cyberduck/core/Profile.java b/core/src/main/java/ch/cyberduck/core/Profile.java index 90201b5c644..af22e735c57 100644 --- a/core/src/main/java/ch/cyberduck/core/Profile.java +++ b/core/src/main/java/ch/cyberduck/core/Profile.java @@ -127,6 +127,11 @@ public Comparator getListComparator() { return parent.getListComparator(); } + @Override + public VersioningMode getVersioningMode() { + return parent.getVersioningMode(); + } + @Override public String getIdentifier() { return parent.getIdentifier(); diff --git a/core/src/main/java/ch/cyberduck/core/Protocol.java b/core/src/main/java/ch/cyberduck/core/Protocol.java index 5a9e9f8ea54..f98888caa26 100644 --- a/core/src/main/java/ch/cyberduck/core/Protocol.java +++ b/core/src/main/java/ch/cyberduck/core/Protocol.java @@ -71,6 +71,8 @@ public interface Protocol extends Comparable, Serializable { */ Comparator getListComparator(); + VersioningMode getVersioningMode(); + /** * @return True if anonymous login is possible. */ @@ -351,6 +353,27 @@ enum Statefulness { stateless } + enum VersioningMode { + /** + * No versioning enabled when uploading files + */ + none { + + }, + /** + * Versioning is handled by storage implementation + */ + storage { + + }, + /** + * Custom implementation using directory to save previous versions + */ + custom { + + }; + } + @SuppressWarnings("unchecked") T getFeature(final Class type); } diff --git a/ctera/src/main/java/ch/cyberduck/core/ctera/CteraProtocol.java b/ctera/src/main/java/ch/cyberduck/core/ctera/CteraProtocol.java index b25ed2d3472..1e5c744aaa3 100644 --- a/ctera/src/main/java/ch/cyberduck/core/ctera/CteraProtocol.java +++ b/ctera/src/main/java/ch/cyberduck/core/ctera/CteraProtocol.java @@ -80,6 +80,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.explicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override public String getTokenPlaceholder() { return "CTERA Token"; diff --git a/dracoon/src/main/java/ch/cyberduck/core/sds/SDSProtocol.java b/dracoon/src/main/java/ch/cyberduck/core/sds/SDSProtocol.java index 373d504e641..bad81b01684 100644 --- a/dracoon/src/main/java/ch/cyberduck/core/sds/SDSProtocol.java +++ b/dracoon/src/main/java/ch/cyberduck/core/sds/SDSProtocol.java @@ -103,6 +103,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.explicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + public enum Authorization { sql, radius, diff --git a/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxProtocol.java b/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxProtocol.java index 2cbf71fdb0e..8f91db633f4 100644 --- a/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxProtocol.java +++ b/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxProtocol.java @@ -72,6 +72,11 @@ public String getPasswordPlaceholder() { return LocaleFactory.localizedString("Authorization code", "Credentials"); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override public Scheme getScheme() { return Scheme.https; diff --git a/eue/src/main/java/ch/cyberduck/core/eue/EueProtocol.java b/eue/src/main/java/ch/cyberduck/core/eue/EueProtocol.java index 911a459e65e..b5a4b88ade4 100644 --- a/eue/src/main/java/ch/cyberduck/core/eue/EueProtocol.java +++ b/eue/src/main/java/ch/cyberduck/core/eue/EueProtocol.java @@ -58,6 +58,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } + @Override public T getFeature(final Class type) { if(type == ComparisonService.class) { diff --git a/ftp/src/main/java/ch/cyberduck/core/ftp/FTPProtocol.java b/ftp/src/main/java/ch/cyberduck/core/ftp/FTPProtocol.java index c0e3081dddf..1f87ce004c7 100644 --- a/ftp/src/main/java/ch/cyberduck/core/ftp/FTPProtocol.java +++ b/ftp/src/main/java/ch/cyberduck/core/ftp/FTPProtocol.java @@ -70,4 +70,10 @@ public boolean isEncodingConfigurable() { public boolean isAnonymousConfigurable() { return true; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } + } diff --git a/ftp/src/main/java/ch/cyberduck/core/ftp/FTPTLSProtocol.java b/ftp/src/main/java/ch/cyberduck/core/ftp/FTPTLSProtocol.java index 7c224e49e7d..f69930620cb 100644 --- a/ftp/src/main/java/ch/cyberduck/core/ftp/FTPTLSProtocol.java +++ b/ftp/src/main/java/ch/cyberduck/core/ftp/FTPTLSProtocol.java @@ -89,4 +89,10 @@ public boolean isCertificateConfigurable() { public boolean isAnonymousConfigurable() { return true; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } + } diff --git a/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveProtocol.java b/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveProtocol.java index ed9ff507351..c4687351e37 100644 --- a/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveProtocol.java +++ b/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveProtocol.java @@ -76,4 +76,9 @@ public Scheme getScheme() { public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.explicit; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } } diff --git a/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageProtocol.java b/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageProtocol.java index 6189358e585..72f66bbfd35 100644 --- a/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageProtocol.java +++ b/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageProtocol.java @@ -122,6 +122,11 @@ public Comparator getListComparator() { return new DefaultLexicographicOrderComparator(); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/hubic/src/main/java/ch/cyberduck/core/hubic/HubicProtocol.java b/hubic/src/main/java/ch/cyberduck/core/hubic/HubicProtocol.java index 04a65ef4c32..9e246c40b62 100644 --- a/hubic/src/main/java/ch/cyberduck/core/hubic/HubicProtocol.java +++ b/hubic/src/main/java/ch/cyberduck/core/hubic/HubicProtocol.java @@ -97,4 +97,9 @@ public String disk() { public String icon() { return new SwiftProtocol().icon(); } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.none; + } } diff --git a/irods/src/main/java/ch/cyberduck/core/irods/IRODSProtocol.java b/irods/src/main/java/ch/cyberduck/core/irods/IRODSProtocol.java index d018a0d8046..20fa56feaf3 100644 --- a/irods/src/main/java/ch/cyberduck/core/irods/IRODSProtocol.java +++ b/irods/src/main/java/ch/cyberduck/core/irods/IRODSProtocol.java @@ -59,4 +59,9 @@ public String disk() { public String getPrefix() { return String.format("%s.%s", IRODSProtocol.class.getPackage().getName(), StringUtils.upperCase(this.getType().name())); } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.none; + } } diff --git a/manta/src/main/java/ch/cyberduck/core/manta/MantaProtocol.java b/manta/src/main/java/ch/cyberduck/core/manta/MantaProtocol.java index 4bb5cfd865b..9091264e8e6 100644 --- a/manta/src/main/java/ch/cyberduck/core/manta/MantaProtocol.java +++ b/manta/src/main/java/ch/cyberduck/core/manta/MantaProtocol.java @@ -79,4 +79,9 @@ public boolean isPasswordConfigurable() { public boolean isPrivateKeyConfigurable() { return true; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } } diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudProtocol.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudProtocol.java index 5e6e5c549ab..8c12a86a962 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudProtocol.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudProtocol.java @@ -66,6 +66,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override public T getFeature(final Class type) { if(type == ComparisonService.class) { diff --git a/nio/src/main/java/ch/cyberduck/core/nio/LocalProtocol.java b/nio/src/main/java/ch/cyberduck/core/nio/LocalProtocol.java index 9bb4ed30e2d..f5a04a0a368 100644 --- a/nio/src/main/java/ch/cyberduck/core/nio/LocalProtocol.java +++ b/nio/src/main/java/ch/cyberduck/core/nio/LocalProtocol.java @@ -116,4 +116,9 @@ public Case getCaseSensitivity() { public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.none; + } } diff --git a/onedrive/src/main/java/ch/cyberduck/core/onedrive/GraphProtocol.java b/onedrive/src/main/java/ch/cyberduck/core/onedrive/GraphProtocol.java index 8386fc77230..540f7134b99 100644 --- a/onedrive/src/main/java/ch/cyberduck/core/onedrive/GraphProtocol.java +++ b/onedrive/src/main/java/ch/cyberduck/core/onedrive/GraphProtocol.java @@ -58,6 +58,11 @@ public String disk() { return "onedrive.tiff"; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override public T getFeature(final Class type) { if(type == ComparisonService.class) { diff --git a/onedrive/src/main/java/ch/cyberduck/core/onedrive/OneDriveProtocol.java b/onedrive/src/main/java/ch/cyberduck/core/onedrive/OneDriveProtocol.java index fae146da20f..c187e41475e 100644 --- a/onedrive/src/main/java/ch/cyberduck/core/onedrive/OneDriveProtocol.java +++ b/onedrive/src/main/java/ch/cyberduck/core/onedrive/OneDriveProtocol.java @@ -40,4 +40,9 @@ public String getPrefix() { public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.explicit; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } } diff --git a/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointProtocol.java b/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointProtocol.java index 6affe572a65..cceaab34134 100644 --- a/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointProtocol.java +++ b/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointProtocol.java @@ -40,4 +40,9 @@ public String getName() { public String getPrefix() { return String.format("%s.%s", SharepointProtocol.class.getPackage().getName(), "Sharepoint"); } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } } diff --git a/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointSiteProtocol.java b/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointSiteProtocol.java index 45bbeb4fb69..e86f04f0bef 100644 --- a/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointSiteProtocol.java +++ b/onedrive/src/main/java/ch/cyberduck/core/onedrive/SharepointSiteProtocol.java @@ -40,4 +40,9 @@ public String getName() { public String getPrefix() { return String.format("%s.%s", SharepointProtocol.class.getPackage().getName(), "SharepointSite"); } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } } diff --git a/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftProtocol.java b/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftProtocol.java index e4d2b816f2b..ed2f1d94556 100644 --- a/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftProtocol.java +++ b/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftProtocol.java @@ -72,6 +72,11 @@ public Comparator getListComparator() { return new DefaultLexicographicOrderComparator(); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudProtocol.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudProtocol.java index 23890f8e63b..cb122006203 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudProtocol.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudProtocol.java @@ -66,6 +66,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override public T getFeature(final Class type) { if(type == ComparisonService.class) { diff --git a/s3/src/main/java/ch/cyberduck/core/s3/S3Protocol.java b/s3/src/main/java/ch/cyberduck/core/s3/S3Protocol.java index 02b75b7343b..b9498ca3192 100644 --- a/s3/src/main/java/ch/cyberduck/core/s3/S3Protocol.java +++ b/s3/src/main/java/ch/cyberduck/core/s3/S3Protocol.java @@ -167,6 +167,11 @@ public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.explicit; } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/spectra/src/main/java/ch/cyberduck/core/spectra/SpectraProtocol.java b/spectra/src/main/java/ch/cyberduck/core/spectra/SpectraProtocol.java index 16f9ed1d71a..e02be860ea8 100644 --- a/spectra/src/main/java/ch/cyberduck/core/spectra/SpectraProtocol.java +++ b/spectra/src/main/java/ch/cyberduck/core/spectra/SpectraProtocol.java @@ -87,6 +87,11 @@ public String getAuthorization() { return S3Protocol.AuthenticationHeaderSignatureVersion.AWS2.name(); } + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.storage; + } + @Override @SuppressWarnings("unchecked") public T getFeature(final Class type) { diff --git a/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPProtocol.java b/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPProtocol.java index b25e51b10af..b6b43e9ab90 100644 --- a/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPProtocol.java +++ b/ssh/src/main/java/ch/cyberduck/core/sftp/SFTPProtocol.java @@ -94,4 +94,9 @@ public JumphostConfigurator getJumpHostFinder() { public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } } diff --git a/webdav/src/main/java/ch/cyberduck/core/dav/DAVProtocol.java b/webdav/src/main/java/ch/cyberduck/core/dav/DAVProtocol.java index 29a5b6a0174..8ff66dd9669 100644 --- a/webdav/src/main/java/ch/cyberduck/core/dav/DAVProtocol.java +++ b/webdav/src/main/java/ch/cyberduck/core/dav/DAVProtocol.java @@ -72,4 +72,9 @@ public CredentialsConfigurator getCredentialsFinder() { public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } } diff --git a/webdav/src/main/java/ch/cyberduck/core/dav/DAVSSLProtocol.java b/webdav/src/main/java/ch/cyberduck/core/dav/DAVSSLProtocol.java index 0b01d3f3c41..a28fec5c482 100644 --- a/webdav/src/main/java/ch/cyberduck/core/dav/DAVSSLProtocol.java +++ b/webdav/src/main/java/ch/cyberduck/core/dav/DAVSSLProtocol.java @@ -84,4 +84,9 @@ public CredentialsConfigurator getCredentialsFinder() { public DirectoryTimestamp getDirectoryTimestamp() { return DirectoryTimestamp.implicit; } + + @Override + public VersioningMode getVersioningMode() { + return VersioningMode.custom; + } } From a56e9b9f6ce94bec8254d2bbca604befdd1ab75d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Mar 2023 11:11:39 +0100 Subject: [PATCH 02/33] Flag if versioning should be enabled in upload transfer. --- .../core/transfer/upload/UploadFilterOptions.java | 9 +++++++++ defaults/src/main/resources/default.properties | 1 + 2 files changed, 10 insertions(+) diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java index cfb9dbe74dc..efef05beb91 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java @@ -32,6 +32,10 @@ public final class UploadFilterOptions { * Create temporary filename with an UUID and rename when upload is complete */ public boolean temporary; + /** + * Upload to versioning directory and copy to final target + */ + public boolean versioning; /** * Enable server side encryption if available */ @@ -52,6 +56,11 @@ public UploadFilterOptions(final Host bookmark) { acl = preferences.getBoolean("queue.upload.acl.change"); timestamp = preferences.getBoolean("queue.upload.timestamp.change"); temporary = preferences.getBoolean("queue.upload.file.temporary"); + switch(bookmark.getProtocol().getVersioningMode()) { + case custom: + versioning = preferences.getBoolean("queue.upload.file.versioning"); + break; + } metadata = preferences.getBoolean("queue.upload.file.metadata.change"); encryption = preferences.getBoolean("queue.upload.file.encryption.change"); redundancy = preferences.getBoolean("queue.upload.file.redundancy.change"); diff --git a/defaults/src/main/resources/default.properties b/defaults/src/main/resources/default.properties index f3bbe65b045..03bccc851a6 100644 --- a/defaults/src/main/resources/default.properties +++ b/defaults/src/main/resources/default.properties @@ -180,6 +180,7 @@ queue.upload.file.temporary=false # Format string for temporary filename. Default to filename-uuid queue.upload.file.temporary.format={0}-{1} queue.upload.file.rename.format={0} ({1}){2} +queue.upload.file.versioning=false queue.download.file.rename.format={0} ({1}){2} queue.download.permissions.change=true queue.download.permissions.default=false From 867376721d63f9e14fa973b3f8b3501ddf46c58e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 17 Mar 2023 12:02:53 +0100 Subject: [PATCH 03/33] Versioning feature to list and revert versions in pre-defined directory. With versioning option in transfer, save additional copy in versioned directory on upload. --- .../main/java/ch/cyberduck/core/Session.java | 8 ++ .../core/shared/DefaultVersioningFeature.java | 95 +++++++++++++++++++ .../transfer/upload/AbstractUploadFilter.java | 63 ++++++++++-- 3 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java diff --git a/core/src/main/java/ch/cyberduck/core/Session.java b/core/src/main/java/ch/cyberduck/core/Session.java index ab452eb1656..b8f927217b7 100644 --- a/core/src/main/java/ch/cyberduck/core/Session.java +++ b/core/src/main/java/ch/cyberduck/core/Session.java @@ -31,6 +31,7 @@ import ch.cyberduck.core.features.Read; import ch.cyberduck.core.features.Search; import ch.cyberduck.core.features.Upload; +import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.preferences.Preferences; import ch.cyberduck.core.preferences.PreferencesFactory; @@ -44,6 +45,7 @@ import ch.cyberduck.core.shared.DefaultSearchFeature; import ch.cyberduck.core.shared.DefaultUploadFeature; import ch.cyberduck.core.shared.DefaultUrlProvider; +import ch.cyberduck.core.shared.DefaultVersioningFeature; import ch.cyberduck.core.shared.DelegatingHomeFeature; import ch.cyberduck.core.shared.DisabledBulkFeature; import ch.cyberduck.core.shared.DisabledMoveFeature; @@ -340,6 +342,12 @@ public T _getFeature(final Class type) { if(type == Home.class) { return (T) new DelegatingHomeFeature(new WorkdirHomeFeature(host), new DefaultPathHomeFeature(host)); } + if(type == Versioning.class) { + switch(host.getProtocol().getVersioningMode()) { + case custom: + return (T) new DefaultVersioningFeature(this); + } + } return host.getProtocol().getFeature(type); } diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java new file mode 100644 index 00000000000..a2dfb2012ec --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -0,0 +1,95 @@ +package ch.cyberduck.core.shared; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import ch.cyberduck.core.AttributedList; +import ch.cyberduck.core.DisabledConnectionCallback; +import ch.cyberduck.core.ListProgressListener; +import ch.cyberduck.core.ListService; +import ch.cyberduck.core.PasswordCallback; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.VersioningConfiguration; +import ch.cyberduck.core.date.ISO8601DateFormatter; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.UnsupportedException; +import ch.cyberduck.core.features.Copy; +import ch.cyberduck.core.features.Versioning; +import ch.cyberduck.core.io.DisabledStreamListener; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.ui.comparator.FilenameComparator; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.EnumSet; +import java.util.TimeZone; + +public class DefaultVersioningFeature implements Versioning { + + private static final String DIRECTORY_SUFFIX = ".cyberduckversions"; + + private final Session session; + + public DefaultVersioningFeature(final Session session) { + this.session = session; + } + + @Override + public VersioningConfiguration getConfiguration(final Path container) throws BackgroundException { + return new VersioningConfiguration(new HostPreferences(session.getHost()).getBoolean("versioning.enable")); + } + + @Override + public void setConfiguration(final Path container, final PasswordCallback prompt, final VersioningConfiguration configuration) throws BackgroundException { + throw new UnsupportedException(); + } + + @Override + public void revert(final Path file) throws BackgroundException { + session.getFeature(Copy.class).copy(file, new Path(file.getParent().getParent(), + StringUtils.removeEnd(file.getParent().getName(), DIRECTORY_SUFFIX), EnumSet.of(Path.Type.file)), new TransferStatus(), + new DisabledConnectionCallback(), new DisabledStreamListener()); + } + + @Override + public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + final AttributedList versions = new AttributedList<>(); + for(Path version : session.getFeature(ListService.class).list(getVersionedFolder(file), listener)) { + version.attributes().setDuplicate(true); + versions.add(version); + } + return versions.filter(new FilenameComparator(false)); + } + + /** + * @param file File to edit + * @return Directory to save previous versions of file + */ + public static Path getVersionedFolder(final Path file) { + return new Path(file.getParent(), DIRECTORY_SUFFIX, EnumSet.of(Path.Type.directory)); + } + + public static Path getVersionedFile(final Path file) { + final String basename = String.format("%s-%s", FilenameUtils.getBaseName(file.getName()), + new ISO8601DateFormatter().format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY)); + if(StringUtils.isNotBlank(file.getExtension())) { + return new Path(getVersionedFolder(file), String.format("%s.%s", basename, file.getExtension()), EnumSet.of(Path.Type.file)); + } + return new Path(getVersionedFolder(file), basename, EnumSet.of(Path.Type.file)); + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java index 9f2f48c638f..1ba42d8259d 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java @@ -38,7 +38,9 @@ import ch.cyberduck.core.exception.NotfoundException; import ch.cyberduck.core.features.AclPermission; import ch.cyberduck.core.features.AttributesFinder; +import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Encryption; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Headers; @@ -48,8 +50,10 @@ import ch.cyberduck.core.features.UnixPermission; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.io.ChecksumCompute; +import ch.cyberduck.core.io.DisabledStreamListener; import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.preferences.PreferencesReader; +import ch.cyberduck.core.shared.DefaultVersioningFeature; import ch.cyberduck.core.transfer.TransferPathFilter; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; @@ -64,7 +68,8 @@ public abstract class AbstractUploadFilter implements TransferPathFilter { private static final Logger log = LogManager.getLogger(AbstractUploadFilter.class); - private final PreferencesReader preferences; + private final PreferencesReader preferences + ; private final Session session; private final SymlinkResolver symlinkResolver; private final Filter hidden = SearchFilterFactory.HIDDEN_FILTER; @@ -172,6 +177,24 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer log.warn(String.format("Cannot use temporary filename for upload with missing rename support for %s", file)); } } + if(options.versioning) { + final Path version = DefaultVersioningFeature.getVersionedFile(file); + if(session.getFeature(Copy.class).isSupported(file, version)) { + final Path versions = DefaultVersioningFeature.getVersionedFolder(file); + if(!find.find(versions)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", versions)); + } + session.getFeature(Directory.class).mkdir(versions, new TransferStatus()); + } + if(log.isDebugEnabled()) { + log.debug(String.format("Add new version %s", version)); + } + status.withRename(version).withDisplayname(file); + // Remember status of target file for later copy + status.getDisplayname().exists(status.isExists()); + } + } status.withMime(new MappingMimeTypeService().getMime(file.getName())); } if(file.isDirectory()) { @@ -310,9 +333,25 @@ public void apply(final Path file, final Local local, final TransferStatus statu if(log.isDebugEnabled()) { log.debug(String.format("Clear exist flag for file %s", local)); } - // Reset exist flag after subclass hae applied strategy + // Reset exist flag after subclass has applied strategy status.setExists(false); } + if(status.isExists()) { + if(options.versioning) { + final Path version = DefaultVersioningFeature.getVersionedFile(file); + if(!session.getFeature(Copy.class).isSupported(file, version)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Rename existing file %s to %s", file, version)); + } + session.getFeature(Move.class).move(file, version, + new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); + if(log.isDebugEnabled()) { + log.debug(String.format("Clear exist flag for file %s", file)); + } + status.exists(false).getDisplayname().exists(false); + } + } + } } @Override @@ -368,12 +407,22 @@ public void complete(final Path file, final Local local, } if(file.isFile()) { if(status.getDisplayname().remote != null) { - final Move move = session.getFeature(Move.class); - if(log.isInfoEnabled()) { - log.info(String.format("Rename file %s to %s", file, status.getDisplayname().remote)); + if(options.versioning) { + final Copy feature = session.getFeature(Copy.class); + if(log.isInfoEnabled()) { + log.info(String.format("Copy file %s to %s", file, status.getDisplayname().remote)); + } + feature.copy(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), + new DisabledConnectionCallback(), new DisabledStreamListener()); + } + else { + final Move feature = session.getFeature(Move.class); + if(log.isInfoEnabled()) { + log.info(String.format("Rename file %s to %s", file, status.getDisplayname().remote)); + } + feature.move(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), + new Delete.DisabledCallback(), new DisabledConnectionCallback()); } - move.move(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), - new Delete.DisabledCallback(), new DisabledConnectionCallback()); } } } From 603204895851950bdc2388a0ad7a7d3986c62c51 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sat, 18 Mar 2023 18:12:20 +0100 Subject: [PATCH 04/33] Merge date formatter implementations. --- ...rmatter.java => ISO8601DateFormatter.java} | 2 +- .../core/date/ISO8601DateParser.java | 204 ------------------ ...est.java => ISO8601DateFormatterTest.java} | 12 +- .../core/googledrive/DriveWriteFeature.java | 4 +- .../GoogleStorageWriteFeature.java | 4 +- .../importer/SmartFtpBookmarkCollection.java | 4 +- .../SwiftAttributesFinderFeature.java | 6 +- .../core/openstack/SwiftSegmentService.java | 6 +- .../auth/AWSSessionCredentialsRetriever.java | 4 +- 9 files changed, 22 insertions(+), 224 deletions(-) rename core/src/main/java/ch/cyberduck/core/date/{RFC3339DateFormatter.java => ISO8601DateFormatter.java} (96%) delete mode 100644 core/src/main/java/ch/cyberduck/core/date/ISO8601DateParser.java rename core/src/test/java/ch/cyberduck/core/date/{RFC3339DateFormatterTest.java => ISO8601DateFormatterTest.java} (65%) diff --git a/core/src/main/java/ch/cyberduck/core/date/RFC3339DateFormatter.java b/core/src/main/java/ch/cyberduck/core/date/ISO8601DateFormatter.java similarity index 96% rename from core/src/main/java/ch/cyberduck/core/date/RFC3339DateFormatter.java rename to core/src/main/java/ch/cyberduck/core/date/ISO8601DateFormatter.java index f012f8bde47..f97f067a3e2 100644 --- a/core/src/main/java/ch/cyberduck/core/date/RFC3339DateFormatter.java +++ b/core/src/main/java/ch/cyberduck/core/date/ISO8601DateFormatter.java @@ -24,7 +24,7 @@ import com.google.gson.internal.bind.util.ISO8601Utils; -public class RFC3339DateFormatter implements DateFormatter { +public class ISO8601DateFormatter implements DateFormatter { @Override public String format(final Date input, final TimeZone zone) { diff --git a/core/src/main/java/ch/cyberduck/core/date/ISO8601DateParser.java b/core/src/main/java/ch/cyberduck/core/date/ISO8601DateParser.java deleted file mode 100644 index 3c02be8a0b9..00000000000 --- a/core/src/main/java/ch/cyberduck/core/date/ISO8601DateParser.java +++ /dev/null @@ -1,204 +0,0 @@ -package ch.cyberduck.core.date; - -/* - * Copyright (c) 2002-2013 David Kocher. All rights reserved. - * http://cyberduck.ch/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * Bug fixes, suggestions and comments should be sent to: - * feedback@cyberduck.ch - */ - -import org.apache.commons.lang3.StringUtils; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.NoSuchElementException; -import java.util.StringTokenizer; -import java.util.TimeZone; - -/** - * Date parser for ISO 8601 format - * http://www.w3.org/TR/1998/NOTE-datetime-19980827 - * - * @author Beno�t Mah� (bmahe@w3.org) - * @author Yves Lafon (ylafon@w3.org) - */ -public final class ISO8601DateParser { - - public ISO8601DateParser() { - // - } - - private boolean check(final StringTokenizer st, final String token) - throws InvalidDateException { - try { - if(st.nextToken().equals(token)) { - return true; - } - else { - throw new InvalidDateException(String.format("Missing [%s]", token)); - } - } - catch(NoSuchElementException ex) { - return false; - } - } - - private Calendar getCalendar(final String isodate) throws InvalidDateException { - // YYYY-MM-DDThh:mm:ss.sTZD - StringTokenizer st = new StringTokenizer(isodate, "-T:.+Z", true); - - Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); - calendar.clear(); - try { - // Year - if(st.hasMoreTokens()) { - int year = Integer.parseInt(st.nextToken()); - calendar.set(Calendar.YEAR, year); - } - else { - return calendar; - } - // Month - if(check(st, "-") && (st.hasMoreTokens())) { - int month = Integer.parseInt(st.nextToken()) - 1; - calendar.set(Calendar.MONTH, month); - } - else { - return calendar; - } - // Day - if(check(st, "-") && (st.hasMoreTokens())) { - int day = Integer.parseInt(st.nextToken()); - calendar.set(Calendar.DAY_OF_MONTH, day); - } - else { - return calendar; - } - // Hour - if(check(st, "T") && (st.hasMoreTokens())) { - int hour = Integer.parseInt(st.nextToken()); - calendar.set(Calendar.HOUR_OF_DAY, hour); - } - else { - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; - } - // Minutes - if(check(st, ":") && (st.hasMoreTokens())) { - int minutes = Integer.parseInt(st.nextToken()); - calendar.set(Calendar.MINUTE, minutes); - } - else { - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar; - } - - // - // Not mandatory now - // - - // Secondes - if(!st.hasMoreTokens()) { - return calendar; - } - String tok = st.nextToken(); - if(tok.equals(":")) { // secondes - if(st.hasMoreTokens()) { - int secondes = Integer.parseInt(st.nextToken()); - calendar.set(Calendar.SECOND, secondes); - if(!st.hasMoreTokens()) { - return calendar; - } - // frac sec - tok = st.nextToken(); - if(tok.equals(".")) { - // bug fixed, thx to Martin Bottcher - String nt = st.nextToken(); - while(nt.length() < 3) { - nt += "0"; - } - nt = nt.substring(0, 3); //Cut trailing chars.. - int millisec = Integer.parseInt(nt); - //int millisec = Integer.parseInt(st.nextToken()) * 10; - calendar.set(Calendar.MILLISECOND, millisec); - if(!st.hasMoreTokens()) { - return calendar; - } - tok = st.nextToken(); - } - else { - calendar.set(Calendar.MILLISECOND, 0); - } - } - else { - throw new InvalidDateException("No seconds specified"); - } - } - else { - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - } - // Timezone - if(!tok.equals("Z")) { // UTC - if(!(tok.equals("+") || tok.equals("-"))) { - throw new InvalidDateException("only Z, + or - allowed"); - } - boolean plus = tok.equals("+"); - if(!st.hasMoreTokens()) { - throw new InvalidDateException("Missing hour field"); - } - int tzhour = Integer.parseInt(st.nextToken()); - int tzmin; - if(check(st, ":") && (st.hasMoreTokens())) { - tzmin = Integer.parseInt(st.nextToken()); - } - else { - throw new InvalidDateException("Missing minute field"); - } - if(plus) { - calendar.add(Calendar.HOUR, -tzhour); - calendar.add(Calendar.MINUTE, -tzmin); - } - else { - calendar.add(Calendar.HOUR, tzhour); - calendar.add(Calendar.MINUTE, tzmin); - } - } - } - catch(NumberFormatException ex) { - throw new InvalidDateException(String.format("[%s] is not an integer", ex.getMessage()), ex); - } - return calendar; - } - - /** - * Parse the given string in ISO 8601 format and build a Date object. - * - * @param input the date in ISO 8601 format - * @return a Date instance - * @throws InvalidDateException if the date is not valid - */ - public Date parse(final String input) throws InvalidDateException { - if(StringUtils.isBlank(input)) { - throw new InvalidDateException(); - } - return this.getCalendar(input).getTime(); - } -} diff --git a/core/src/test/java/ch/cyberduck/core/date/RFC3339DateFormatterTest.java b/core/src/test/java/ch/cyberduck/core/date/ISO8601DateFormatterTest.java similarity index 65% rename from core/src/test/java/ch/cyberduck/core/date/RFC3339DateFormatterTest.java rename to core/src/test/java/ch/cyberduck/core/date/ISO8601DateFormatterTest.java index ab299785e52..c1c2f14d3c5 100644 --- a/core/src/test/java/ch/cyberduck/core/date/RFC3339DateFormatterTest.java +++ b/core/src/test/java/ch/cyberduck/core/date/ISO8601DateFormatterTest.java @@ -21,21 +21,23 @@ import static org.junit.Assert.assertEquals; -public class RFC3339DateFormatterTest { +public class ISO8601DateFormatterTest { @Test public void testParseWithoutMilliseconds() throws Exception { - assertEquals(1667567722000L, new RFC3339DateFormatter().parse("2022-11-04T13:15:22Z").getTime(), 0L); + assertEquals(1667567722000L, new ISO8601DateFormatter().parse("2022-11-04T13:15:22Z").getTime(), 0L); + assertEquals(1667567722000L, new ISO8601DateFormatter().parse("20221104T131522Z").getTime(), 0L); + assertEquals(1679159330000L, new ISO8601DateFormatter().parse("20230318T180941.516+0100").getTime(), 0L); } @Test public void testParseWithMilliseconds() throws Exception { - assertEquals(1667567722123L, new RFC3339DateFormatter().parse("2022-11-04T13:15:22.123Z").getTime(), 0L); + assertEquals(1667567722123L, new ISO8601DateFormatter().parse("2022-11-04T13:15:22.123Z").getTime(), 0L); } @Test public void testPrint() { - assertEquals("2022-11-04T12:43:42.654+01:00", new RFC3339DateFormatter().format(1667562222654L, TimeZone.getTimeZone("Europe/Zurich"))); - assertEquals("2022-11-04T11:43:42.654Z", new RFC3339DateFormatter().format(1667562222654L, TimeZone.getTimeZone("UTC"))); + assertEquals("2022-11-04T12:43:42.654+01:00", new ISO8601DateFormatter().format(1667562222654L, TimeZone.getTimeZone("Europe/Zurich"))); + assertEquals("2022-11-04T11:43:42.654Z", new ISO8601DateFormatter().format(1667562222654L, TimeZone.getTimeZone("UTC"))); } } \ No newline at end of file diff --git a/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveWriteFeature.java b/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveWriteFeature.java index b75315a5481..53141b7ce88 100644 --- a/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveWriteFeature.java +++ b/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveWriteFeature.java @@ -17,7 +17,7 @@ import ch.cyberduck.core.ConnectionCallback; import ch.cyberduck.core.Path; -import ch.cyberduck.core.date.RFC3339DateFormatter; +import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.http.AbstractHttpWriteFeature; @@ -100,7 +100,7 @@ public File call(final AbstractHttpEntity entity) throws BackgroundException { metadata.append(String.format("\"name\":\"%s\"", file.getName())); if(null != status.getTimestamp()) { metadata.append(String.format(",\"modifiedTime\":\"%s\"", - new RFC3339DateFormatter().format(status.getTimestamp(), TimeZone.getTimeZone("UTC")))); + new ISO8601DateFormatter().format(status.getTimestamp(), TimeZone.getTimeZone("UTC")))); } if(StringUtils.isNotBlank(status.getMime())) { metadata.append(String.format(",\"mimeType\":\"%s\"", status.getMime())); diff --git a/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageWriteFeature.java b/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageWriteFeature.java index 2d11d176f04..26f6a87b96f 100644 --- a/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageWriteFeature.java +++ b/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageWriteFeature.java @@ -19,7 +19,7 @@ import ch.cyberduck.core.ConnectionCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathContainerService; -import ch.cyberduck.core.date.RFC3339DateFormatter; +import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.http.AbstractHttpWriteFeature; @@ -125,7 +125,7 @@ else if(Acl.CANNED_BUCKET_OWNER_READ.equals(status.getAcl())) { } if(null != status.getTimestamp()) { metadata.append(String.format(", \"customTime\": \"%s\"", - new RFC3339DateFormatter().format(status.getTimestamp(), TimeZone.getTimeZone("UTC")))); + new ISO8601DateFormatter().format(status.getTimestamp(), TimeZone.getTimeZone("UTC")))); } metadata.append("}"); request.setEntity(new StringEntity(metadata.toString(), diff --git a/importer/src/main/java/ch/cyberduck/core/importer/SmartFtpBookmarkCollection.java b/importer/src/main/java/ch/cyberduck/core/importer/SmartFtpBookmarkCollection.java index 0176a4df82a..4db23f0b1e2 100644 --- a/importer/src/main/java/ch/cyberduck/core/importer/SmartFtpBookmarkCollection.java +++ b/importer/src/main/java/ch/cyberduck/core/importer/SmartFtpBookmarkCollection.java @@ -24,7 +24,7 @@ import ch.cyberduck.core.LocalFactory; import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.Scheme; -import ch.cyberduck.core.date.ISO8601DateParser; +import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.date.InvalidDateException; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.ftp.FTPConnectMode; @@ -146,7 +146,7 @@ public void endElement(String name, String elementText) { break; case "LastConnect": try { - current.setTimestamp(new ISO8601DateParser().parse(elementText)); + current.setTimestamp(new ISO8601DateFormatter().parse(elementText)); } catch(InvalidDateException e) { log.warn(String.format("Failed to parse timestamp from %s %s", elementText, e.getMessage())); diff --git a/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftAttributesFinderFeature.java b/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftAttributesFinderFeature.java index 41aff9c64da..a1e090ad3da 100644 --- a/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftAttributesFinderFeature.java +++ b/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftAttributesFinderFeature.java @@ -25,7 +25,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.PathContainerService; -import ch.cyberduck.core.date.ISO8601DateParser; +import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.date.InvalidDateException; import ch.cyberduck.core.date.RFC1123DateFormatter; import ch.cyberduck.core.exception.BackgroundException; @@ -56,7 +56,7 @@ public class SwiftAttributesFinderFeature implements AttributesFinder, Attribute private final SwiftSession session; private final PathContainerService containerService = new DefaultPathContainerService(); private final RFC1123DateFormatter rfc1123DateFormatter = new RFC1123DateFormatter(); - private final ISO8601DateParser iso8601DateParser = new ISO8601DateParser(); + private final ISO8601DateFormatter iso8601DateFormatter = new ISO8601DateFormatter(); private final SwiftRegionService regionService; public SwiftAttributesFinderFeature(SwiftSession session) { @@ -150,7 +150,7 @@ public PathAttributes toAttributes(final StorageObject object) { final String lastModified = object.getLastModified(); if(lastModified != null) { try { - attributes.setModificationDate(iso8601DateParser.parse(lastModified).getTime()); + attributes.setModificationDate(iso8601DateFormatter.parse(lastModified).getTime()); } catch(InvalidDateException e) { log.warn(String.format("%s is not ISO 8601 format %s", lastModified, e.getMessage())); diff --git a/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftSegmentService.java b/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftSegmentService.java index 5cca0d636db..69a043fa218 100644 --- a/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftSegmentService.java +++ b/openstack/src/main/java/ch/cyberduck/core/openstack/SwiftSegmentService.java @@ -21,7 +21,7 @@ import ch.cyberduck.core.DefaultPathContainerService; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathContainerService; -import ch.cyberduck.core.date.ISO8601DateParser; +import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.date.InvalidDateException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.io.Checksum; @@ -55,8 +55,8 @@ public class SwiftSegmentService { private final PathContainerService containerService = new DefaultPathContainerService(); - private final ISO8601DateParser dateParser - = new ISO8601DateParser(); + private final ISO8601DateFormatter dateParser + = new ISO8601DateFormatter(); /** * Segement files prefix diff --git a/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java b/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java index 9ee35193fba..cc0491b4cd1 100644 --- a/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java +++ b/s3/src/main/java/ch/cyberduck/core/auth/AWSSessionCredentialsRetriever.java @@ -29,7 +29,7 @@ import ch.cyberduck.core.ProtocolFactory; import ch.cyberduck.core.TemporaryAccessTokens; import ch.cyberduck.core.TranscriptListener; -import ch.cyberduck.core.date.ISO8601DateParser; +import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.date.InvalidDateException; import ch.cyberduck.core.dav.DAVReadFeature; import ch.cyberduck.core.dav.DAVSession; @@ -156,7 +156,7 @@ protected Credentials parse(final InputStream in) throws BackgroundException { break; case "Expiration": try { - expiration = new ISO8601DateParser().parse(value); + expiration = new ISO8601DateFormatter().parse(value); } catch(InvalidDateException e) { log.warn(String.format("Failure %s parsing %s", e, value)); From ff104f50260298bbb6b171c778fd38a78f5d41c5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 19 Mar 2023 12:59:06 +0100 Subject: [PATCH 05/33] Filter by filename. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index a2dfb2012ec..04867c6f491 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -38,6 +38,7 @@ import java.util.EnumSet; import java.util.TimeZone; +import java.util.stream.Collectors; public class DefaultVersioningFeature implements Versioning { @@ -69,7 +70,8 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { final AttributedList versions = new AttributedList<>(); - for(Path version : session.getFeature(ListService.class).list(getVersionedFolder(file), listener)) { + for(Path version : session.getFeature(ListService.class).list(file.isFile() ? getVersionedFolder(file) : file, listener).toStream() + .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { version.attributes().setDuplicate(true); versions.add(version); } From c1582605074b6b8c9f360f2f5a6b47169aecb6bf Mon Sep 17 00:00:00 2001 From: David Kocher Date: Mon, 20 Mar 2023 08:24:24 +0100 Subject: [PATCH 06/33] Bulk feature to delete previous versions given limit in preferences after upload. --- .../main/java/ch/cyberduck/core/Session.java | 7 +- .../shared/DefaultVersioningBulkFeature.java | 71 +++++++++++++++++++ .../core/shared/DisabledBulkFeature.java | 5 +- .../src/main/resources/default.properties | 2 + 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java diff --git a/core/src/main/java/ch/cyberduck/core/Session.java b/core/src/main/java/ch/cyberduck/core/Session.java index b8f927217b7..dcfd01e396b 100644 --- a/core/src/main/java/ch/cyberduck/core/Session.java +++ b/core/src/main/java/ch/cyberduck/core/Session.java @@ -45,6 +45,7 @@ import ch.cyberduck.core.shared.DefaultSearchFeature; import ch.cyberduck.core.shared.DefaultUploadFeature; import ch.cyberduck.core.shared.DefaultUrlProvider; +import ch.cyberduck.core.shared.DefaultVersioningBulkFeature; import ch.cyberduck.core.shared.DefaultVersioningFeature; import ch.cyberduck.core.shared.DelegatingHomeFeature; import ch.cyberduck.core.shared.DisabledBulkFeature; @@ -101,7 +102,7 @@ public boolean alert(final ConnectionCallback callback) throws BackgroundExcepti return false; } return preferences.getBoolean( - String.format("connection.unsecure.warning.%s", host.getProtocol().getScheme())); + String.format("connection.unsecure.warning.%s", host.getProtocol().getScheme())); } public Session withListener(final TranscriptListener listener) { @@ -313,6 +314,10 @@ public T _getFeature(final Class type) { return (T) new DefaultDownloadFeature(this.getFeature(Read.class)); } if(type == Bulk.class) { + switch(host.getProtocol().getVersioningMode()) { + case custom: + return (T) new DefaultVersioningBulkFeature(this); + } return (T) new DisabledBulkFeature(); } if(type == Move.class) { diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java new file mode 100644 index 00000000000..90ab180c94b --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java @@ -0,0 +1,71 @@ +package ch.cyberduck.core.shared; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import ch.cyberduck.core.ConnectionCallback; +import ch.cyberduck.core.DisabledListProgressListener; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Bulk; +import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.Transfer; +import ch.cyberduck.core.transfer.TransferItem; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.ui.comparator.FilenameComparator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DefaultVersioningBulkFeature extends DisabledBulkFeature { + private static final Logger log = LogManager.getLogger(DefaultVersioningBulkFeature.class); + + private final Session session; + private Delete delete; + + public DefaultVersioningBulkFeature(final Session session) { + this.session = session; + this.delete = session.getFeature(Delete.class); + } + + @Override + public void post(final Transfer.Type type, final Map files, final ConnectionCallback callback) throws BackgroundException { + switch(type) { + case upload: + if(new HostPreferences(session.getHost()).getBoolean("queue.upload.file.versioning")) { + for(TransferItem item : files.keySet()) { + final List versions = new DefaultVersioningFeature(session).list(item.remote, new DisabledListProgressListener()).toStream() + .sorted(new FilenameComparator(false)).skip(new HostPreferences(session.getHost()).getInteger("queue.upload.file.versioning.limit")).collect(Collectors.toList()); + if(log.isWarnEnabled()) { + log.warn(String.format("Delete %d previous versions of %s", versions.size(), item.remote)); + } + delete.delete(versions, callback, new Delete.DisabledCallback()); + } + } + } + } + + @Override + public Bulk withDelete(final Delete delete) { + this.delete = delete; + return this; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/shared/DisabledBulkFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DisabledBulkFeature.java index 6003d19c87e..e196b383182 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DisabledBulkFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DisabledBulkFeature.java @@ -16,6 +16,7 @@ */ import ch.cyberduck.core.ConnectionCallback; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.features.Bulk; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.transfer.Transfer; @@ -26,7 +27,7 @@ public class DisabledBulkFeature implements Bulk> { @Override - public Map pre(final Transfer.Type type, final Map files, final ConnectionCallback callback) { + public Map pre(final Transfer.Type type, final Map files, final ConnectionCallback callback) throws BackgroundException { return null; } @@ -36,7 +37,7 @@ public Bulk> withDelete(final Delete delete) { } @Override - public void post(final Transfer.Type type, final Map files, final ConnectionCallback callback) { + public void post(final Transfer.Type type, final Map files, final ConnectionCallback callback) throws BackgroundException { // } } diff --git a/defaults/src/main/resources/default.properties b/defaults/src/main/resources/default.properties index 03bccc851a6..8e72356a316 100644 --- a/defaults/src/main/resources/default.properties +++ b/defaults/src/main/resources/default.properties @@ -181,6 +181,8 @@ queue.upload.file.temporary=false queue.upload.file.temporary.format={0}-{1} queue.upload.file.rename.format={0} ({1}){2} queue.upload.file.versioning=false +# Only keep most recent versions +queue.upload.file.versioning.limit=5 queue.download.file.rename.format={0} ({1}){2} queue.download.permissions.change=true queue.download.permissions.default=false From 8b0d876af4574a845343091ab2bff6a03a2b76f2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Fri, 24 Mar 2023 17:29:02 +0100 Subject: [PATCH 07/33] Rewrite as regular upload filter. --- .../cyberduck/core/features/Versioning.java | 12 ++- .../core/shared/DefaultVersioningFeature.java | 3 +- .../core/transfer/TransferAction.java | 15 ++++ .../core/transfer/UploadTransfer.java | 4 + .../transfer/upload/AbstractUploadFilter.java | 60 ++------------ .../transfer/upload/VersioningCopyFilter.java | 81 +++++++++++++++++++ .../upload/VersioningRenameFilter.java | 68 ++++++++++++++++ 7 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java diff --git a/core/src/main/java/ch/cyberduck/core/features/Versioning.java b/core/src/main/java/ch/cyberduck/core/features/Versioning.java index b7ea5b1beb9..a504e13bc17 100644 --- a/core/src/main/java/ch/cyberduck/core/features/Versioning.java +++ b/core/src/main/java/ch/cyberduck/core/features/Versioning.java @@ -58,7 +58,7 @@ public interface Versioning { * @param file File * @return True if this file version can be reverted */ - default boolean isRevertable(Path file) { + default boolean isRevertable(final Path file) { return file.attributes().isDuplicate(); } @@ -71,4 +71,14 @@ default boolean isRevertable(Path file) { * @throws BackgroundException Failure reading versions from server */ AttributedList list(Path file, ListProgressListener listener) throws BackgroundException; + + /** + * Generate new versioned path for file + * + * @param file File + * @return Same + */ + default Path toVersioned(final Path file) { + return file; + } } diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 04867c6f491..f777c47c27e 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -86,7 +86,8 @@ public static Path getVersionedFolder(final Path file) { return new Path(file.getParent(), DIRECTORY_SUFFIX, EnumSet.of(Path.Type.directory)); } - public static Path getVersionedFile(final Path file) { + @Override + public Path toVersioned(final Path file) { final String basename = String.format("%s-%s", FilenameUtils.getBaseName(file.getName()), new ISO8601DateFormatter().format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY)); if(StringUtils.isNotBlank(file.getExtension())) { diff --git a/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java b/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java index 8f5ff45a718..9a5f9806189 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java @@ -197,6 +197,21 @@ public String getDescription() { } }; + /** + * Move existing file to versioning directory + */ + public static final TransferAction versioning = new TransferAction("versioning") { + @Override + public String getTitle() { + return LocaleFactory.localizedString("Versioning", "Transfer"); + } + + @Override + public String getDescription() { + return LocaleFactory.localizedString("Move existing file to versioned folder", "Transfer"); + } + }; + public static final TransferAction cancel = new TransferAction("cancel") { @Override public String getTitle() { diff --git a/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java b/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java index 32d73001855..963054bdf76 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java @@ -56,6 +56,7 @@ import ch.cyberduck.core.transfer.upload.SkipFilter; import ch.cyberduck.core.transfer.upload.UploadFilterOptions; import ch.cyberduck.core.transfer.upload.UploadRegexPriorityComparator; +import ch.cyberduck.core.transfer.upload.VersioningRenameFilter; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -182,6 +183,9 @@ public AbstractUploadFilter filter(final Session source, final Session des if(action.equals(TransferAction.comparison)) { return new CompareFilter(resolver, source, options, listener).withFinder(find).withAttributes(attributes); } + if(action.equals(TransferAction.versioning)) { + return new VersioningRenameFilter(resolver, source, options).withFinder(find).withAttributes(attributes); + } return new OverwriteFilter(resolver, source, options).withFinder(find).withAttributes(attributes); } diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java index 1ba42d8259d..8f138197cfe 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java @@ -38,9 +38,7 @@ import ch.cyberduck.core.exception.NotfoundException; import ch.cyberduck.core.features.AclPermission; import ch.cyberduck.core.features.AttributesFinder; -import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Delete; -import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Encryption; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Headers; @@ -50,10 +48,8 @@ import ch.cyberduck.core.features.UnixPermission; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.io.ChecksumCompute; -import ch.cyberduck.core.io.DisabledStreamListener; import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.preferences.PreferencesReader; -import ch.cyberduck.core.shared.DefaultVersioningFeature; import ch.cyberduck.core.transfer.TransferPathFilter; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; @@ -177,24 +173,6 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer log.warn(String.format("Cannot use temporary filename for upload with missing rename support for %s", file)); } } - if(options.versioning) { - final Path version = DefaultVersioningFeature.getVersionedFile(file); - if(session.getFeature(Copy.class).isSupported(file, version)) { - final Path versions = DefaultVersioningFeature.getVersionedFolder(file); - if(!find.find(versions)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", versions)); - } - session.getFeature(Directory.class).mkdir(versions, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Add new version %s", version)); - } - status.withRename(version).withDisplayname(file); - // Remember status of target file for later copy - status.getDisplayname().exists(status.isExists()); - } - } status.withMime(new MappingMimeTypeService().getMime(file.getName())); } if(file.isDirectory()) { @@ -333,25 +311,9 @@ public void apply(final Path file, final Local local, final TransferStatus statu if(log.isDebugEnabled()) { log.debug(String.format("Clear exist flag for file %s", local)); } - // Reset exist flag after subclass has applied strategy + // Reset exist flag after subclass hae applied strategy status.setExists(false); } - if(status.isExists()) { - if(options.versioning) { - final Path version = DefaultVersioningFeature.getVersionedFile(file); - if(!session.getFeature(Copy.class).isSupported(file, version)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Rename existing file %s to %s", file, version)); - } - session.getFeature(Move.class).move(file, version, - new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); - if(log.isDebugEnabled()) { - log.debug(String.format("Clear exist flag for file %s", file)); - } - status.exists(false).getDisplayname().exists(false); - } - } - } } @Override @@ -407,22 +369,12 @@ public void complete(final Path file, final Local local, } if(file.isFile()) { if(status.getDisplayname().remote != null) { - if(options.versioning) { - final Copy feature = session.getFeature(Copy.class); - if(log.isInfoEnabled()) { - log.info(String.format("Copy file %s to %s", file, status.getDisplayname().remote)); - } - feature.copy(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), - new DisabledConnectionCallback(), new DisabledStreamListener()); - } - else { - final Move feature = session.getFeature(Move.class); - if(log.isInfoEnabled()) { - log.info(String.format("Rename file %s to %s", file, status.getDisplayname().remote)); - } - feature.move(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), - new Delete.DisabledCallback(), new DisabledConnectionCallback()); + final Move move = session.getFeature(Move.class); + if(log.isInfoEnabled()) { + log.info(String.format("Rename file %s to %s", file, status.getDisplayname().remote)); } + move.move(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), + new Delete.DisabledCallback(), new DisabledConnectionCallback()); } } } diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java new file mode 100644 index 00000000000..f819ee7fbe1 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java @@ -0,0 +1,81 @@ +package ch.cyberduck.core.transfer.upload; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import ch.cyberduck.core.DisabledConnectionCallback; +import ch.cyberduck.core.Local; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Copy; +import ch.cyberduck.core.features.Directory; +import ch.cyberduck.core.features.Versioning; +import ch.cyberduck.core.io.DisabledStreamListener; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.transfer.symlink.SymlinkResolver; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class VersioningCopyFilter extends AbstractUploadFilter { + private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); + + private final Session session; + private final Versioning versioning; + + public VersioningCopyFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { + super(symlinkResolver, session, options); + this.session = session; + this.versioning = session.getFeature(Versioning.class); + } + + @Override + public TransferStatus prepare(final Path file, final Local local, final TransferStatus parent, final ProgressListener progress) throws BackgroundException { + final TransferStatus status = super.prepare(file, local, parent, progress); + final Path version = versioning.toVersioned(file); + if(session.getFeature(Copy.class).isSupported(file, version)) { + final Path directory = file.getParent(); + if(!find.find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } + if(log.isDebugEnabled()) { + log.debug(String.format("Add new version %s", version)); + } + status.withRename(version).withDisplayname(file); + // Remember status of target file for later copy + status.getDisplayname().exists(status.isExists()); + } + return status; + } + + @Override + public void complete(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { + if(file.isFile()) { + if(status.getDisplayname().remote != null) { + final Copy feature = session.getFeature(Copy.class); + if(log.isInfoEnabled()) { + log.info(String.format("Copy file %s to %s", file, status.getDisplayname().remote)); + } + feature.copy(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), new DisabledConnectionCallback(), new DisabledStreamListener()); + } + } + super.complete(file, local, status, listener); + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java new file mode 100644 index 00000000000..998986e8929 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java @@ -0,0 +1,68 @@ +package ch.cyberduck.core.transfer.upload; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import ch.cyberduck.core.DisabledConnectionCallback; +import ch.cyberduck.core.Local; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.features.Directory; +import ch.cyberduck.core.features.Move; +import ch.cyberduck.core.features.Versioning; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.transfer.symlink.SymlinkResolver; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class VersioningRenameFilter extends AbstractUploadFilter { + private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); + + private final Session session; + private final Versioning versioning; + + public VersioningRenameFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { + super(symlinkResolver, session, options); + this.session = session; + this.versioning = session.getFeature(Versioning.class); + } + + @Override + public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { + if(status.isExists()) { + final Path version = versioning.toVersioned(file); + final Path directory = version.getParent(); + if(!find.find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } + if(log.isDebugEnabled()) { + log.debug(String.format("Rename existing file %s to %s", file, version)); + } + session.getFeature(Move.class).move(file, version, + new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); + if(log.isDebugEnabled()) { + log.debug(String.format("Clear exist flag for file %s", file)); + } + status.exists(false).getDisplayname().exists(false); + } + } +} \ No newline at end of file From 2edee91bea1ed4acff3123f538fd0baccc97bc5c Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sat, 25 Mar 2023 12:16:32 +0100 Subject: [PATCH 08/33] Check for existing file. --- .../transfer/upload/VersioningCopyFilter.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java index f819ee7fbe1..6eb03dc6305 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java @@ -46,21 +46,23 @@ public VersioningCopyFilter(final SymlinkResolver symlinkResolver, final @Override public TransferStatus prepare(final Path file, final Local local, final TransferStatus parent, final ProgressListener progress) throws BackgroundException { final TransferStatus status = super.prepare(file, local, parent, progress); - final Path version = versioning.toVersioned(file); - if(session.getFeature(Copy.class).isSupported(file, version)) { - final Path directory = file.getParent(); - if(!find.find(directory)) { + if(status.isExists()) { + final Path version = versioning.toVersioned(file); + if(session.getFeature(Copy.class).isSupported(file, version)) { + final Path directory = file.getParent(); + if(!find.find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", directory)); + log.debug(String.format("Add new version %s", version)); } - session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Add new version %s", version)); + status.withRename(version).withDisplayname(file); + // Remember status of target file for later copy + status.getDisplayname().exists(status.isExists()); } - status.withRename(version).withDisplayname(file); - // Remember status of target file for later copy - status.getDisplayname().exists(status.isExists()); } return status; } From 10ea79021e88a0a04ab3574753c4827ed1f6a562 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sat, 25 Mar 2023 12:17:40 +0100 Subject: [PATCH 09/33] Add check for filename pattern. --- .../transfer/upload/VersioningCopyFilter.java | 31 +++++++++------ .../upload/VersioningRenameFilter.java | 39 ++++++++++++------- .../src/main/resources/default.properties | 1 + 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java index 6eb03dc6305..b3cf464d881 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java @@ -24,13 +24,17 @@ import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Versioning; +import ch.cyberduck.core.filter.UploadRegexFilter; import ch.cyberduck.core.io.DisabledStreamListener; +import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.regex.Pattern; + public class VersioningCopyFilter extends AbstractUploadFilter { private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); @@ -47,21 +51,24 @@ public VersioningCopyFilter(final SymlinkResolver symlinkResolver, final public TransferStatus prepare(final Path file, final Local local, final TransferStatus parent, final ProgressListener progress) throws BackgroundException { final TransferStatus status = super.prepare(file, local, parent, progress); if(status.isExists()) { - final Path version = versioning.toVersioned(file); - if(session.getFeature(Copy.class).isSupported(file, version)) { - final Path directory = file.getParent(); - if(!find.find(directory)) { + final String regex = new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex"); + if(new UploadRegexFilter(Pattern.compile(regex)).accept(local)) { + final Path version = versioning.toVersioned(file); + if(session.getFeature(Copy.class).isSupported(file, version)) { + final Path directory = file.getParent(); + if(!find.find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", directory)); + log.debug(String.format("Add new version %s", version)); } - session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Add new version %s", version)); + status.withRename(version).withDisplayname(file); + // Remember status of target file for later copy + status.getDisplayname().exists(status.isExists()); } - status.withRename(version).withDisplayname(file); - // Remember status of target file for later copy - status.getDisplayname().exists(status.isExists()); } } return status; diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java index 998986e8929..7b2bbb3efc4 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java @@ -25,12 +25,17 @@ import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Versioning; +import ch.cyberduck.core.filter.UploadRegexFilter; +import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.List; +import java.util.regex.Pattern; + public class VersioningRenameFilter extends AbstractUploadFilter { private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); @@ -46,23 +51,31 @@ public VersioningRenameFilter(final SymlinkResolver symlinkResolver, fina @Override public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { if(status.isExists()) { - final Path version = versioning.toVersioned(file); - final Path directory = version.getParent(); - if(!find.find(directory)) { + final String regex = new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex"); + if(new UploadRegexFilter(Pattern.compile(regex)).accept(local)) { + final Path version = versioning.toVersioned(file); + final Path directory = version.getParent(); + if(!find.find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", directory)); + log.debug(String.format("Rename existing file %s to %s", file, version)); } - session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Rename existing file %s to %s", file, version)); + session.getFeature(Move.class).move(file, version, + new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); + if(log.isDebugEnabled()) { + log.debug(String.format("Clear exist flag for file %s", file)); + } + status.exists(false).getDisplayname().exists(false); } - session.getFeature(Move.class).move(file, version, - new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); - if(log.isDebugEnabled()) { - log.debug(String.format("Clear exist flag for file %s", file)); + else { + if(log.isDebugEnabled()) { + log.debug(String.format("No match for %s in %s", file.getName(), regex)); + } } - status.exists(false).getDisplayname().exists(false); } } } \ No newline at end of file diff --git a/defaults/src/main/resources/default.properties b/defaults/src/main/resources/default.properties index 8e72356a316..795e74555fb 100644 --- a/defaults/src/main/resources/default.properties +++ b/defaults/src/main/resources/default.properties @@ -181,6 +181,7 @@ queue.upload.file.temporary=false queue.upload.file.temporary.format={0}-{1} queue.upload.file.rename.format={0} ({1}){2} queue.upload.file.versioning=false +queue.upload.file.versioning.include.regex=.* # Only keep most recent versions queue.upload.file.versioning.limit=5 queue.download.file.rename.format={0} ({1}){2} From ba67b2fe0e103600a804d2f2ab45f2a45069c584 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sat, 25 Mar 2023 12:18:24 +0100 Subject: [PATCH 10/33] Add check for support of rename operation. --- .../upload/VersioningRenameFilter.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java index 7b2bbb3efc4..4b7a7e41536 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java @@ -21,6 +21,7 @@ import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Move; @@ -54,22 +55,24 @@ public void apply(final Path file, final Local local, final TransferStatus statu final String regex = new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex"); if(new UploadRegexFilter(Pattern.compile(regex)).accept(local)) { final Path version = versioning.toVersioned(file); - final Path directory = version.getParent(); - if(!find.find(directory)) { + if(session.getFeature(Move.class).isSupported(file, version)) { + final Path directory = version.getParent(); + if(!find.find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", directory)); + log.debug(String.format("Rename existing file %s to %s", file, version)); } - session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Rename existing file %s to %s", file, version)); - } - session.getFeature(Move.class).move(file, version, - new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); - if(log.isDebugEnabled()) { - log.debug(String.format("Clear exist flag for file %s", file)); + session.getFeature(Move.class).move(file, version, + new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); + if(log.isDebugEnabled()) { + log.debug(String.format("Clear exist flag for file %s", file)); + } + status.exists(false).getDisplayname().exists(false); } - status.exists(false).getDisplayname().exists(false); } else { if(log.isDebugEnabled()) { From f71d214bad2ab4783854dd0a43d550c6c20685a8 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Tue, 28 Mar 2023 21:39:24 +0200 Subject: [PATCH 11/33] Allow custom date format. --- .../cyberduck/core/shared/DefaultVersioningFeature.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index f777c47c27e..adf81a1b37e 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -23,6 +23,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.Session; import ch.cyberduck.core.VersioningConfiguration; +import ch.cyberduck.core.date.DateFormatter; import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.UnsupportedException; @@ -45,9 +46,15 @@ public class DefaultVersioningFeature implements Versioning { private static final String DIRECTORY_SUFFIX = ".cyberduckversions"; private final Session session; + private final DateFormatter formatter; public DefaultVersioningFeature(final Session session) { + this(session, new ISO8601DateFormatter()); + } + + public DefaultVersioningFeature(final Session session, final DateFormatter formatter) { this.session = session; + this.formatter = formatter; } @Override @@ -89,7 +96,7 @@ public static Path getVersionedFolder(final Path file) { @Override public Path toVersioned(final Path file) { final String basename = String.format("%s-%s", FilenameUtils.getBaseName(file.getName()), - new ISO8601DateFormatter().format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY)); + formatter.format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY)); if(StringUtils.isNotBlank(file.getExtension())) { return new Path(getVersionedFolder(file), String.format("%s.%s", basename, file.getExtension()), EnumSet.of(Path.Type.file)); } From 44274522198682d16259263bd524345fd9362361 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 16:10:32 +0200 Subject: [PATCH 12/33] Review include pattern. --- .../core/transfer/upload/VersioningCopyFilter.java | 6 +++--- .../core/transfer/upload/VersioningRenameFilter.java | 10 ++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java index b3cf464d881..e3f0040d8a7 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java @@ -24,7 +24,6 @@ import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Versioning; -import ch.cyberduck.core.filter.UploadRegexFilter; import ch.cyberduck.core.io.DisabledStreamListener; import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.transfer.TransferStatus; @@ -40,19 +39,20 @@ public class VersioningCopyFilter extends AbstractUploadFilter { private final Session session; private final Versioning versioning; + private final Pattern include; public VersioningCopyFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { super(symlinkResolver, session, options); this.session = session; this.versioning = session.getFeature(Versioning.class); + this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); } @Override public TransferStatus prepare(final Path file, final Local local, final TransferStatus parent, final ProgressListener progress) throws BackgroundException { final TransferStatus status = super.prepare(file, local, parent, progress); if(status.isExists()) { - final String regex = new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex"); - if(new UploadRegexFilter(Pattern.compile(regex)).accept(local)) { + if(include.matcher(file.getName()).matches()) { final Path version = versioning.toVersioned(file); if(session.getFeature(Copy.class).isSupported(file, version)) { final Path directory = file.getParent(); diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java index 4b7a7e41536..db42a587394 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java @@ -21,12 +21,10 @@ import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Versioning; -import ch.cyberduck.core.filter.UploadRegexFilter; import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; @@ -34,7 +32,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.List; import java.util.regex.Pattern; public class VersioningRenameFilter extends AbstractUploadFilter { @@ -42,18 +39,19 @@ public class VersioningRenameFilter extends AbstractUploadFilter { private final Session session; private final Versioning versioning; + private final Pattern include; public VersioningRenameFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { super(symlinkResolver, session, options); this.session = session; this.versioning = session.getFeature(Versioning.class); + this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); } @Override public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { if(status.isExists()) { - final String regex = new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex"); - if(new UploadRegexFilter(Pattern.compile(regex)).accept(local)) { + if(include.matcher(file.getName()).matches()) { final Path version = versioning.toVersioned(file); if(session.getFeature(Move.class).isSupported(file, version)) { final Path directory = version.getParent(); @@ -76,7 +74,7 @@ public void apply(final Path file, final Local local, final TransferStatus statu } else { if(log.isDebugEnabled()) { - log.debug(String.format("No match for %s in %s", file.getName(), regex)); + log.debug(String.format("No match for %s in %s", file.getName(), include)); } } } From d770d0d275ca7de17c217a050de6b472d3a198c2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 16:36:58 +0200 Subject: [PATCH 13/33] Refactor moving logic to feature. --- .../cyberduck/core/features/Versioning.java | 20 ++--- .../core/shared/DefaultVersioningFeature.java | 49 +++++++++- .../transfer/upload/VersioningCopyFilter.java | 90 ------------------- .../upload/VersioningRenameFilter.java | 37 +------- .../VaultRegistryVersioningFeature.java | 5 ++ .../features/CryptoVersioningFeature.java | 5 ++ 6 files changed, 70 insertions(+), 136 deletions(-) delete mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java diff --git a/core/src/main/java/ch/cyberduck/core/features/Versioning.java b/core/src/main/java/ch/cyberduck/core/features/Versioning.java index a504e13bc17..36e3aea05bd 100644 --- a/core/src/main/java/ch/cyberduck/core/features/Versioning.java +++ b/core/src/main/java/ch/cyberduck/core/features/Versioning.java @@ -44,6 +44,16 @@ public interface Versioning { */ void setConfiguration(Path container, PasswordCallback prompt, VersioningConfiguration configuration) throws BackgroundException; + /** + * Save new version + * + * @param file File or folder + * @return True if version is saved + */ + default boolean save(Path file) throws BackgroundException { + return false; + } + /** * Restore this version * @@ -71,14 +81,4 @@ default boolean isRevertable(final Path file) { * @throws BackgroundException Failure reading versions from server */ AttributedList list(Path file, ListProgressListener listener) throws BackgroundException; - - /** - * Generate new versioned path for file - * - * @param file File - * @return Same - */ - default Path toVersioned(final Path file) { - return file; - } } diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index adf81a1b37e..963a2d22ea7 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -28,6 +28,10 @@ import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.UnsupportedException; import ch.cyberduck.core.features.Copy; +import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.features.Directory; +import ch.cyberduck.core.features.Find; +import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.io.DisabledStreamListener; import ch.cyberduck.core.preferences.HostPreferences; @@ -36,17 +40,22 @@ import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import java.util.EnumSet; import java.util.TimeZone; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class DefaultVersioningFeature implements Versioning { + private static final Logger log = LogManager.getLogger(DefaultVersioningFeature.class); private static final String DIRECTORY_SUFFIX = ".cyberduckversions"; private final Session session; private final DateFormatter formatter; + private final Pattern include; public DefaultVersioningFeature(final Session session) { this(session, new ISO8601DateFormatter()); @@ -55,6 +64,7 @@ public DefaultVersioningFeature(final Session session) { public DefaultVersioningFeature(final Session session, final DateFormatter formatter) { this.session = session; this.formatter = formatter; + this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); } @Override @@ -67,6 +77,35 @@ public void setConfiguration(final Path container, final PasswordCallback prompt throw new UnsupportedException(); } + @Override + public boolean save(final Path file) throws BackgroundException { + if(include.matcher(file.getName()).matches()) { + final Path version = this.toVersioned(file); + if(!session.getFeature(Move.class).isSupported(file, version)) { + return false; + } + final Path directory = version.getParent(); + if(!session.getFeature(Find.class).find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } + if(log.isDebugEnabled()) { + log.debug(String.format("Rename existing file %s to %s", file, version)); + } + session.getFeature(Move.class).move(file, version, + new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); + return true; + } + else { + if(log.isDebugEnabled()) { + log.debug(String.format("No match for %s in %s", file.getName(), include)); + } + return false; + } + } + @Override public void revert(final Path file) throws BackgroundException { session.getFeature(Copy.class).copy(file, new Path(file.getParent().getParent(), @@ -93,8 +132,14 @@ public static Path getVersionedFolder(final Path file) { return new Path(file.getParent(), DIRECTORY_SUFFIX, EnumSet.of(Path.Type.directory)); } - @Override - public Path toVersioned(final Path file) { + + /** + * Generate new versioned path for file + * + * @param file File + * @return Same + */ + private Path toVersioned(final Path file) { final String basename = String.format("%s-%s", FilenameUtils.getBaseName(file.getName()), formatter.format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY)); if(StringUtils.isNotBlank(file.getExtension())) { diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java deleted file mode 100644 index e3f0040d8a7..00000000000 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningCopyFilter.java +++ /dev/null @@ -1,90 +0,0 @@ -package ch.cyberduck.core.transfer.upload; - -/* - * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. - * https://cyberduck.io/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -import ch.cyberduck.core.DisabledConnectionCallback; -import ch.cyberduck.core.Local; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProgressListener; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.features.Copy; -import ch.cyberduck.core.features.Directory; -import ch.cyberduck.core.features.Versioning; -import ch.cyberduck.core.io.DisabledStreamListener; -import ch.cyberduck.core.preferences.HostPreferences; -import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.core.transfer.symlink.SymlinkResolver; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.regex.Pattern; - -public class VersioningCopyFilter extends AbstractUploadFilter { - private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); - - private final Session session; - private final Versioning versioning; - private final Pattern include; - - public VersioningCopyFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { - super(symlinkResolver, session, options); - this.session = session; - this.versioning = session.getFeature(Versioning.class); - this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); - } - - @Override - public TransferStatus prepare(final Path file, final Local local, final TransferStatus parent, final ProgressListener progress) throws BackgroundException { - final TransferStatus status = super.prepare(file, local, parent, progress); - if(status.isExists()) { - if(include.matcher(file.getName()).matches()) { - final Path version = versioning.toVersioned(file); - if(session.getFeature(Copy.class).isSupported(file, version)) { - final Path directory = file.getParent(); - if(!find.find(directory)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", directory)); - } - session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Add new version %s", version)); - } - status.withRename(version).withDisplayname(file); - // Remember status of target file for later copy - status.getDisplayname().exists(status.isExists()); - } - } - } - return status; - } - - @Override - public void complete(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { - if(file.isFile()) { - if(status.getDisplayname().remote != null) { - final Copy feature = session.getFeature(Copy.class); - if(log.isInfoEnabled()) { - log.info(String.format("Copy file %s to %s", file, status.getDisplayname().remote)); - } - feature.copy(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), new DisabledConnectionCallback(), new DisabledStreamListener()); - } - } - super.complete(file, local, status, listener); - } -} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java index db42a587394..c3da700d466 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java @@ -15,67 +15,36 @@ * GNU General Public License for more details. */ -import ch.cyberduck.core.DisabledConnectionCallback; import ch.cyberduck.core.Local; import ch.cyberduck.core.Path; import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.Session; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.features.Delete; -import ch.cyberduck.core.features.Directory; -import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Versioning; -import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.regex.Pattern; - public class VersioningRenameFilter extends AbstractUploadFilter { private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); - private final Session session; private final Versioning versioning; - private final Pattern include; public VersioningRenameFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { super(symlinkResolver, session, options); - this.session = session; this.versioning = session.getFeature(Versioning.class); - this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); } @Override public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { if(status.isExists()) { - if(include.matcher(file.getName()).matches()) { - final Path version = versioning.toVersioned(file); - if(session.getFeature(Move.class).isSupported(file, version)) { - final Path directory = version.getParent(); - if(!find.find(directory)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Create directory %s for versions", directory)); - } - session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); - } - if(log.isDebugEnabled()) { - log.debug(String.format("Rename existing file %s to %s", file, version)); - } - session.getFeature(Move.class).move(file, version, - new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); - if(log.isDebugEnabled()) { - log.debug(String.format("Clear exist flag for file %s", file)); - } - status.exists(false).getDisplayname().exists(false); - } - } - else { + if(versioning.save(file)) { if(log.isDebugEnabled()) { - log.debug(String.format("No match for %s in %s", file.getName(), include)); + log.debug(String.format("Clear exist flag for file %s", file)); } + status.exists(false).getDisplayname().exists(false); } } } diff --git a/core/src/main/java/ch/cyberduck/core/vault/registry/VaultRegistryVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/vault/registry/VaultRegistryVersioningFeature.java index df992f6108d..980400e3889 100644 --- a/core/src/main/java/ch/cyberduck/core/vault/registry/VaultRegistryVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/vault/registry/VaultRegistryVersioningFeature.java @@ -58,6 +58,11 @@ public boolean isRevertable(final Path file) { } } + @Override + public boolean save(final Path file) throws BackgroundException { + return registry.find(session, file).getFeature(session, Versioning.class, proxy).save(file); + } + @Override public void revert(final Path file) throws BackgroundException { registry.find(session, file).getFeature(session, Versioning.class, proxy).revert(file); diff --git a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoVersioningFeature.java b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoVersioningFeature.java index 9eeed2d4a3b..debfd30d71c 100644 --- a/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoVersioningFeature.java +++ b/cryptomator/src/main/java/ch/cyberduck/core/cryptomator/features/CryptoVersioningFeature.java @@ -48,6 +48,11 @@ public void setConfiguration(final Path container, final PasswordCallback prompt delegate.setConfiguration(vault.encrypt(session, container), prompt, configuration); } + @Override + public boolean save(final Path file) throws BackgroundException { + return delegate.save(file); + } + @Override public void revert(final Path file) throws BackgroundException { delegate.revert(vault.encrypt(session, file)); From 20c908e43fd72d2837b100f0c9e1d30dc814e360 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 16:40:24 +0200 Subject: [PATCH 14/33] Move files matching versioning criteria instead of delete. --- .../core/shared/DefaultVersioningFeature.java | 2 +- .../cyberduck/core/worker/DeleteWorker.java | 51 ++++++++++++++----- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 963a2d22ea7..eafd3526c1f 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -128,7 +128,7 @@ public AttributedList list(final Path file, final ListProgressListener lis * @param file File to edit * @return Directory to save previous versions of file */ - public static Path getVersionedFolder(final Path file) { + private static Path getVersionedFolder(final Path file) { return new Path(file.getParent(), DIRECTORY_SUFFIX, EnumSet.of(Path.Type.directory)); } diff --git a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java index 091d4ffee53..6373e9d5040 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java @@ -19,7 +19,6 @@ */ import ch.cyberduck.core.Filter; -import ch.cyberduck.core.Host; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.LocaleFactory; @@ -32,7 +31,9 @@ import ch.cyberduck.core.exception.ConnectionCanceledException; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Trash; +import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.preferences.PreferencesFactory; +import ch.cyberduck.core.transfer.TransferAction; import ch.cyberduck.core.transfer.TransferStatus; import org.apache.logging.log4j.LogManager; @@ -41,6 +42,7 @@ import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -112,28 +114,49 @@ public List run(final Session session) throws BackgroundException { if(this.isCanceled()) { throw new ConnectionCanceledException(); } - recursive.putAll(this.compile(session.getHost(), delete, list, new WorkerListProgressListener(this, listener), file)); + recursive.putAll(this.compile(delete, list, new WorkerListProgressListener(this, listener), file)); } // Iterate again to delete any files that can be omitted when recursive operation is supported if(delete.isRecursive()) { recursive.keySet().removeIf(f -> recursive.keySet().stream().anyMatch(f::isChild)); } - delete.delete(recursive, prompt, new Delete.Callback() { - @Override - public void delete(final Path file) { - listener.message(MessageFormat.format(LocaleFactory.localizedString("Deleting {0}", "Status"), file.getName())); - callback.delete(file); - if(file.isDirectory()) { - if(delete.isRecursive()) { - files.stream().filter(f -> f.isChild(file)).forEach(callback::delete); + switch(session.getHost().getProtocol().getVersioningMode()) { + case custom: + if(TransferAction.versioning == TransferAction.forName(PreferencesFactory.get().getProperty("queue.upload.action"))) { + if(log.isDebugEnabled()) { + log.debug(String.format("Delete disabled using %s", TransferAction.versioning)); } + final Versioning versioning = session.getFeature(Versioning.class); + for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { + final Path f = iter.next(); + if(versioning.save(f)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Skip deleting %s", f)); + } + iter.remove(); + } + } + break; } - } - }); + } + if(!recursive.isEmpty()) { + delete.delete(recursive, prompt, new Delete.Callback() { + @Override + public void delete(final Path file) { + listener.message(MessageFormat.format(LocaleFactory.localizedString("Deleting {0}", "Status"), file.getName())); + callback.delete(file); + if(file.isDirectory()) { + if(delete.isRecursive()) { + files.stream().filter(f -> f.isChild(file)).forEach(callback::delete); + } + } + } + }); + } return new ArrayList<>(recursive.keySet()); } - protected Map compile(final Host host, final Delete delete, final ListService list, final ListProgressListener listener, final Path file) throws BackgroundException { + protected Map compile(final Delete delete, final ListService list, final ListProgressListener listener, final Path file) throws BackgroundException { // Compile recursive list final Map recursive = new LinkedHashMap<>(); if(file.isFile() || file.isSymbolicLink()) { @@ -161,7 +184,7 @@ else if(file.isDirectory()) { if(this.isCanceled()) { throw new ConnectionCanceledException(); } - recursive.putAll(this.compile(host, delete, list, listener, child)); + recursive.putAll(this.compile(delete, list, listener, child)); } } // Add parent after children From b7128fad04871e35ae258d2ad006a4ece7ac4a3b Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 16:56:22 +0200 Subject: [PATCH 15/33] Add check if versioned directory exists. --- .../core/shared/DefaultVersioningFeature.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index eafd3526c1f..a108f62342f 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -116,10 +116,13 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { final AttributedList versions = new AttributedList<>(); - for(Path version : session.getFeature(ListService.class).list(file.isFile() ? getVersionedFolder(file) : file, listener).toStream() - .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { - version.attributes().setDuplicate(true); - versions.add(version); + final Path directory = file.isFile() ? getVersionedFolder(file) : file; + if(session.getFeature(Find.class).find(directory)) { + for(Path version : session.getFeature(ListService.class).list(directory, listener).toStream() + .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { + version.attributes().setDuplicate(true); + versions.add(version); + } } return versions.filter(new FilenameComparator(false)); } From 57acdd0eeeee86d037612a60dcbe8c68bba6b48f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 17:19:59 +0200 Subject: [PATCH 16/33] Allow listing versions for directory. --- .../main/java/ch/cyberduck/core/b2/B2VersioningFeature.java | 3 +++ .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 2 +- .../main/java/ch/cyberduck/core/worker/VersionsWorker.java | 3 --- .../main/java/ch/cyberduck/core/sds/SDSVersioningFeature.java | 3 +++ .../ch/cyberduck/core/dropbox/DropboxVersioningFeature.java | 4 +++- .../ch/cyberduck/core/googledrive/DriveVersioningFeature.java | 3 +++ .../core/googlestorage/GoogleStorageVersioningFeature.java | 3 +++ .../cyberduck/core/nextcloud/NextcloudVersioningFeature.java | 3 +++ .../core/onedrive/features/GraphVersioningFeature.java | 3 +++ .../main/java/ch/cyberduck/core/s3/S3VersioningFeature.java | 3 +++ 10 files changed, 25 insertions(+), 5 deletions(-) diff --git a/backblaze/src/main/java/ch/cyberduck/core/b2/B2VersioningFeature.java b/backblaze/src/main/java/ch/cyberduck/core/b2/B2VersioningFeature.java index 953623eed7a..34d0c1ed5bb 100644 --- a/backblaze/src/main/java/ch/cyberduck/core/b2/B2VersioningFeature.java +++ b/backblaze/src/main/java/ch/cyberduck/core/b2/B2VersioningFeature.java @@ -69,6 +69,9 @@ public boolean isRevertable(final Path file) { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } return new B2ObjectListService(session, fileid).list(file, listener).filter(new NullFilter() { @Override public boolean accept(final Path f) { diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index a108f62342f..c814a8e7b6f 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -116,7 +116,7 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { final AttributedList versions = new AttributedList<>(); - final Path directory = file.isFile() ? getVersionedFolder(file) : file; + final Path directory = getVersionedFolder(file); if(session.getFeature(Find.class).find(directory)) { for(Path version : session.getFeature(ListService.class).list(directory, listener).toStream() .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { diff --git a/core/src/main/java/ch/cyberduck/core/worker/VersionsWorker.java b/core/src/main/java/ch/cyberduck/core/worker/VersionsWorker.java index 43fb1338fa7..9d4955cc8d1 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/VersionsWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/VersionsWorker.java @@ -44,9 +44,6 @@ public VersionsWorker(final Path file, final ListProgressListener listener) { @Override public AttributedList run(final Session session) throws BackgroundException { - if(file.isDirectory()) { - return AttributedList.emptyList(); - } final Versioning feature = session.getFeature(Versioning.class); if(log.isDebugEnabled()) { log.debug(String.format("Run with feature %s", feature)); diff --git a/dracoon/src/main/java/ch/cyberduck/core/sds/SDSVersioningFeature.java b/dracoon/src/main/java/ch/cyberduck/core/sds/SDSVersioningFeature.java index 40240d66d55..2ca184aa264 100644 --- a/dracoon/src/main/java/ch/cyberduck/core/sds/SDSVersioningFeature.java +++ b/dracoon/src/main/java/ch/cyberduck/core/sds/SDSVersioningFeature.java @@ -70,6 +70,9 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } final int chunksize = new HostPreferences(session.getHost()).getInteger("sds.listing.chunksize"); try { int offset = 0; diff --git a/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxVersioningFeature.java b/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxVersioningFeature.java index f27a73c8691..1a37bda262a 100644 --- a/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxVersioningFeature.java +++ b/dropbox/src/main/java/ch/cyberduck/core/dropbox/DropboxVersioningFeature.java @@ -17,7 +17,6 @@ import ch.cyberduck.core.AttributedList; import ch.cyberduck.core.ListProgressListener; -import ch.cyberduck.core.NullFilter; import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; @@ -73,6 +72,9 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } try { final AttributedList versions = new AttributedList<>(); final ListRevisionsResult result = new DbxUserFilesRequests(session.getClient(file)).listRevisions(containerService.getKey(file)); diff --git a/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveVersioningFeature.java b/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveVersioningFeature.java index f01fea8940f..1b142de8090 100644 --- a/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveVersioningFeature.java +++ b/googledrive/src/main/java/ch/cyberduck/core/googledrive/DriveVersioningFeature.java @@ -72,6 +72,9 @@ public boolean isRevertable(final Path file) { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } try { final AttributedList versions = new AttributedList<>(); String page = null; diff --git a/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageVersioningFeature.java b/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageVersioningFeature.java index d383b2f9776..dbbf2930041 100644 --- a/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageVersioningFeature.java +++ b/googlestorage/src/main/java/ch/cyberduck/core/googlestorage/GoogleStorageVersioningFeature.java @@ -93,6 +93,9 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } return new GoogleStorageObjectListService(session).list(file, listener).filter(new NullFilter() { @Override public boolean accept(final Path file) { diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java index c53f48c0939..0aabcc57432 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java @@ -91,6 +91,9 @@ public boolean isRevertable(final Path file) { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } try { final AttributedList versions = new AttributedList<>(); // To obtain all the version of a file a normal PROPFIND has to be send diff --git a/onedrive/src/main/java/ch/cyberduck/core/onedrive/features/GraphVersioningFeature.java b/onedrive/src/main/java/ch/cyberduck/core/onedrive/features/GraphVersioningFeature.java index 0e2a1d16de6..ae14623156c 100644 --- a/onedrive/src/main/java/ch/cyberduck/core/onedrive/features/GraphVersioningFeature.java +++ b/onedrive/src/main/java/ch/cyberduck/core/onedrive/features/GraphVersioningFeature.java @@ -74,6 +74,9 @@ public void revert(Path file) throws BackgroundException { @Override public AttributedList list(Path file, ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } final AttributedList versions = new AttributedList<>(); final DriveItem item = session.getItem(file); try { diff --git a/s3/src/main/java/ch/cyberduck/core/s3/S3VersioningFeature.java b/s3/src/main/java/ch/cyberduck/core/s3/S3VersioningFeature.java index 93779dfe6b5..e5fc35e95d1 100644 --- a/s3/src/main/java/ch/cyberduck/core/s3/S3VersioningFeature.java +++ b/s3/src/main/java/ch/cyberduck/core/s3/S3VersioningFeature.java @@ -220,6 +220,9 @@ protected Credentials getToken(final PasswordCallback callback) throws Connectio @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + if(file.isDirectory()) { + return AttributedList.emptyList(); + } return new S3VersionedObjectListService(session, acl).list(file, new ProxyListProgressListener(new IndexedListProgressListener() { @Override public void message(final String message) { From ebe17eb098fff999d2b619eace572bb5ae500b7f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 17:21:46 +0200 Subject: [PATCH 17/33] Revert to upload option instead of filter. --- .../core/transfer/TransferAction.java | 15 ------ .../core/transfer/UploadTransfer.java | 4 -- .../transfer/upload/AbstractUploadFilter.java | 12 +++++ .../transfer/upload/UploadFilterOptions.java | 2 +- .../upload/VersioningRenameFilter.java | 51 ------------------- .../cyberduck/core/worker/DeleteWorker.java | 7 +-- 6 files changed, 15 insertions(+), 76 deletions(-) delete mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java diff --git a/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java b/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java index 9a5f9806189..8f5ff45a718 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/TransferAction.java @@ -197,21 +197,6 @@ public String getDescription() { } }; - /** - * Move existing file to versioning directory - */ - public static final TransferAction versioning = new TransferAction("versioning") { - @Override - public String getTitle() { - return LocaleFactory.localizedString("Versioning", "Transfer"); - } - - @Override - public String getDescription() { - return LocaleFactory.localizedString("Move existing file to versioned folder", "Transfer"); - } - }; - public static final TransferAction cancel = new TransferAction("cancel") { @Override public String getTitle() { diff --git a/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java b/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java index 963054bdf76..32d73001855 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/UploadTransfer.java @@ -56,7 +56,6 @@ import ch.cyberduck.core.transfer.upload.SkipFilter; import ch.cyberduck.core.transfer.upload.UploadFilterOptions; import ch.cyberduck.core.transfer.upload.UploadRegexPriorityComparator; -import ch.cyberduck.core.transfer.upload.VersioningRenameFilter; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -183,9 +182,6 @@ public AbstractUploadFilter filter(final Session source, final Session des if(action.equals(TransferAction.comparison)) { return new CompareFilter(resolver, source, options, listener).withFinder(find).withAttributes(attributes); } - if(action.equals(TransferAction.versioning)) { - return new VersioningRenameFilter(resolver, source, options).withFinder(find).withAttributes(attributes); - } return new OverwriteFilter(resolver, source, options).withFinder(find).withAttributes(attributes); } diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java index 8f138197cfe..9cf0b6b1454 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java @@ -46,6 +46,7 @@ import ch.cyberduck.core.features.Redundancy; import ch.cyberduck.core.features.Timestamp; import ch.cyberduck.core.features.UnixPermission; +import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.features.Write; import ch.cyberduck.core.io.ChecksumCompute; import ch.cyberduck.core.preferences.HostPreferences; @@ -307,6 +308,17 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer @Override public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { + if(status.isExists()) { + if(options.versioning) { + final Versioning feature = session.getFeature(Versioning.class); + if(feature.save(file)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Clear exist flag for file %s", file)); + } + status.exists(false).getDisplayname().exists(false); + } + } + } if(status.getRename().remote != null) { if(log.isDebugEnabled()) { log.debug(String.format("Clear exist flag for file %s", local)); diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java index efef05beb91..62197c17695 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java @@ -33,7 +33,7 @@ public final class UploadFilterOptions { */ public boolean temporary; /** - * Upload to versioning directory and copy to final target + * Move existing file to versioning directory */ public boolean versioning; /** diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java deleted file mode 100644 index c3da700d466..00000000000 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/VersioningRenameFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -package ch.cyberduck.core.transfer.upload; - -/* - * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. - * https://cyberduck.io/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -import ch.cyberduck.core.Local; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.ProgressListener; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.features.Versioning; -import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.core.transfer.symlink.SymlinkResolver; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class VersioningRenameFilter extends AbstractUploadFilter { - private static final Logger log = LogManager.getLogger(VersioningRenameFilter.class); - - private final Versioning versioning; - - public VersioningRenameFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { - super(symlinkResolver, session, options); - this.versioning = session.getFeature(Versioning.class); - } - - @Override - public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { - if(status.isExists()) { - if(versioning.save(file)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Clear exist flag for file %s", file)); - } - status.exists(false).getDisplayname().exists(false); - } - } - } -} \ No newline at end of file diff --git a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java index 6373e9d5040..bb7921c80ee 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java @@ -33,8 +33,8 @@ import ch.cyberduck.core.features.Trash; import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.preferences.PreferencesFactory; -import ch.cyberduck.core.transfer.TransferAction; import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.transfer.upload.UploadFilterOptions; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -122,10 +122,7 @@ public List run(final Session session) throws BackgroundException { } switch(session.getHost().getProtocol().getVersioningMode()) { case custom: - if(TransferAction.versioning == TransferAction.forName(PreferencesFactory.get().getProperty("queue.upload.action"))) { - if(log.isDebugEnabled()) { - log.debug(String.format("Delete disabled using %s", TransferAction.versioning)); - } + if(new UploadFilterOptions(session.getHost()).versioning) { final Versioning versioning = session.getFeature(Versioning.class); for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { final Path f = iter.next(); From f940619add960951a11e66ea9b332406406bae75 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 21:34:23 +0200 Subject: [PATCH 18/33] Allow restoring directories. --- .../core/shared/DefaultVersioningFeature.java | 61 ++++++++++++++----- .../shared/DefaultVersioningFeatureTest.java | 56 +++++++++++++++++ 2 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index c814a8e7b6f..0441fac8a49 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -27,13 +27,11 @@ import ch.cyberduck.core.date.ISO8601DateFormatter; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.UnsupportedException; -import ch.cyberduck.core.features.Copy; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Versioning; -import ch.cyberduck.core.io.DisabledStreamListener; import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.ui.comparator.FilenameComparator; @@ -45,6 +43,7 @@ import java.util.EnumSet; import java.util.TimeZone; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -68,8 +67,8 @@ public DefaultVersioningFeature(final Session session, final DateFormatter fo } @Override - public VersioningConfiguration getConfiguration(final Path container) throws BackgroundException { - return new VersioningConfiguration(new HostPreferences(session.getHost()).getBoolean("versioning.enable")); + public VersioningConfiguration getConfiguration(final Path file) throws BackgroundException { + return new VersioningConfiguration(file.isDirectory() || include.matcher(file.getName()).matches()); } @Override @@ -79,8 +78,8 @@ public void setConfiguration(final Path container, final PasswordCallback prompt @Override public boolean save(final Path file) throws BackgroundException { - if(include.matcher(file.getName()).matches()) { - final Path version = this.toVersioned(file); + if(this.getConfiguration(file).isEnabled()) { + final Path version = toVersioned(file, formatter); if(!session.getFeature(Move.class).isSupported(file, version)) { return false; } @@ -94,6 +93,11 @@ public boolean save(final Path file) throws BackgroundException { if(log.isDebugEnabled()) { log.debug(String.format("Rename existing file %s to %s", file, version)); } + if(file.isDirectory()) { + if(!session.getFeature(Move.class).isRecursive(file, version)) { + throw new UnsupportedException(); + } + } session.getFeature(Move.class).move(file, version, new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); return true; @@ -108,9 +112,19 @@ public boolean save(final Path file) throws BackgroundException { @Override public void revert(final Path file) throws BackgroundException { - session.getFeature(Copy.class).copy(file, new Path(file.getParent().getParent(), - StringUtils.removeEnd(file.getParent().getName(), DIRECTORY_SUFFIX), EnumSet.of(Path.Type.file)), new TransferStatus(), - new DisabledConnectionCallback(), new DisabledStreamListener()); + final Path target = fromVersioned(file); + final TransferStatus status = new TransferStatus().exists(session.getFeature(Find.class).find(target)); + if(status.isExists()) { + if(this.save(target)) { + status.setExists(false); + } + } + if(file.isDirectory()) { + if(!session.getFeature(Move.class).isRecursive(file, target)) { + throw new UnsupportedException(); + } + } + session.getFeature(Move.class).move(file, target, status, new Delete.DisabledCallback(), new DisabledConnectionCallback()); } @Override @@ -135,6 +149,7 @@ private static Path getVersionedFolder(final Path file) { return new Path(file.getParent(), DIRECTORY_SUFFIX, EnumSet.of(Path.Type.directory)); } + private static final char FILENAME_VERSION_SEPARATOR = '-'; /** * Generate new versioned path for file @@ -142,12 +157,30 @@ private static Path getVersionedFolder(final Path file) { * @param file File * @return Same */ - private Path toVersioned(final Path file) { - final String basename = String.format("%s-%s", FilenameUtils.getBaseName(file.getName()), - formatter.format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY)); + static Path toVersioned(final Path file, final DateFormatter formatter) { + // Translate from /basename.extension to /.cyberduckversions/basename-timestamp.extension + final String basename = String.format("%s%s%s", FilenameUtils.getBaseName(file.getName()), + FILENAME_VERSION_SEPARATOR, toTimestamp(formatter)); if(StringUtils.isNotBlank(file.getExtension())) { - return new Path(getVersionedFolder(file), String.format("%s.%s", basename, file.getExtension()), EnumSet.of(Path.Type.file)); + return new Path(getVersionedFolder(file), String.format("%s.%s", basename, file.getExtension()), file.getType()); + } + return new Path(getVersionedFolder(file), basename, file.getType()); + } + + static String toTimestamp(final DateFormatter formatter) { + return formatter.format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY); + } + + static Path fromVersioned(final Path file) { + // Translate from /.cyberduckersions/basename-timestamp.extension to /basename.extension + final Pattern format = Pattern.compile("(.*)-[0-9]{8}T[0-9]{6}\\.[0-9]{3}[Z](\\..*)?"); + final Matcher matcher = format.matcher(file.getName()); + if(matcher.matches()) { + if(StringUtils.isBlank(matcher.group(2))) { + return new Path(file.getParent().getParent(), matcher.group(1), file.getType()); + } + return new Path(file.getParent().getParent(), String.format("%s%s", matcher.group(1), matcher.group(2)), file.getType()); } - return new Path(getVersionedFolder(file), basename, EnumSet.of(Path.Type.file)); + return file; } } diff --git a/core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java b/core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java new file mode 100644 index 00000000000..19cf3abf29f --- /dev/null +++ b/core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java @@ -0,0 +1,56 @@ +package ch.cyberduck.core.shared; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import ch.cyberduck.core.Path; +import ch.cyberduck.core.date.ISO8601DateFormatter; + +import org.junit.Test; + +import java.util.EnumSet; + +import static org.junit.Assert.assertEquals; + +public class DefaultVersioningFeatureTest { + + @Test + public void testToVersioned() { + { + Path f = new Path("/d/f", EnumSet.of(Path.Type.file)); + final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); + assertEquals(".cyberduckversions", versioned.getParent().getName()); + assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); + } + { + Path f = new Path("/d/f.ext", EnumSet.of(Path.Type.file)); + final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); + assertEquals(".cyberduckversions", versioned.getParent().getName()); + assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); + } + { + Path f = new Path("/d/f-f", EnumSet.of(Path.Type.file)); + final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); + assertEquals(".cyberduckversions", versioned.getParent().getName()); + assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); + } + { + Path f = new Path("/d/f-f.ext", EnumSet.of(Path.Type.file)); + final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); + assertEquals(".cyberduckversions", versioned.getParent().getName()); + assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); + } + } +} \ No newline at end of file From 8ec20d9f72c95e3ef476e1f38310e1cbfb5c068d Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 21:52:51 +0200 Subject: [PATCH 19/33] Allow revert when inside versioning directory. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 0441fac8a49..a2dbbc71adf 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -183,4 +183,9 @@ static Path fromVersioned(final Path file) { } return file; } + + @Override + public boolean isRevertable(final Path file) { + return StringUtils.equals(DIRECTORY_SUFFIX, file.getParent().getName()); + } } From 97e2484124bd37282a27dc335357afb3dbcedbb2 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 29 Mar 2023 23:48:31 +0200 Subject: [PATCH 20/33] Generify filename patterns. --- .../core/shared/DefaultVersioningFeature.java | 126 ++++++++++++------ .../shared/DefaultVersioningFeatureTest.java | 56 -------- .../ISO8601FilenameVersionIdentifierTest.java | 47 +++++++ 3 files changed, 129 insertions(+), 100 deletions(-) delete mode 100644 core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java create mode 100644 core/src/test/java/ch/cyberduck/core/shared/ISO8601FilenameVersionIdentifierTest.java diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index a2dbbc71adf..9912fe6916c 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -22,9 +22,11 @@ import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.Session; +import ch.cyberduck.core.SimplePathPredicate; import ch.cyberduck.core.VersioningConfiguration; import ch.cyberduck.core.date.DateFormatter; import ch.cyberduck.core.date.ISO8601DateFormatter; +import ch.cyberduck.core.date.InvalidDateException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.UnsupportedException; import ch.cyberduck.core.features.Delete; @@ -41,6 +43,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.Date; import java.util.EnumSet; import java.util.TimeZone; import java.util.regex.Matcher; @@ -50,18 +53,18 @@ public class DefaultVersioningFeature implements Versioning { private static final Logger log = LogManager.getLogger(DefaultVersioningFeature.class); - private static final String DIRECTORY_SUFFIX = ".cyberduckversions"; - private final Session session; - private final DateFormatter formatter; + private final FilenameVersionIdentifier formatter; + private final VersioningDirectoryProvider provider; private final Pattern include; public DefaultVersioningFeature(final Session session) { - this(session, new ISO8601DateFormatter()); + this(session, new DefaultVersioningDirectoryProvider(), new ISO8601FilenameVersionIdentifier()); } - public DefaultVersioningFeature(final Session session, final DateFormatter formatter) { + public DefaultVersioningFeature(final Session session, final VersioningDirectoryProvider provider, final FilenameVersionIdentifier formatter) { this.session = session; + this.provider = provider; this.formatter = formatter; this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); } @@ -79,7 +82,7 @@ public void setConfiguration(final Path container, final PasswordCallback prompt @Override public boolean save(final Path file) throws BackgroundException { if(this.getConfiguration(file).isEnabled()) { - final Path version = toVersioned(file, formatter); + final Path version = new Path(provider.provide(file), formatter.toVersion(file.getName()), file.getType()); if(!session.getFeature(Move.class).isSupported(file, version)) { return false; } @@ -112,7 +115,7 @@ public boolean save(final Path file) throws BackgroundException { @Override public void revert(final Path file) throws BackgroundException { - final Path target = fromVersioned(file); + final Path target = new Path(file.getParent().getParent(), formatter.fromVersion(file.getName()), file.getType()); final TransferStatus status = new TransferStatus().exists(session.getFeature(Find.class).find(target)); if(status.isExists()) { if(this.save(target)) { @@ -130,7 +133,7 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { final AttributedList versions = new AttributedList<>(); - final Path directory = getVersionedFolder(file); + final Path directory = provider.provide(file); if(session.getFeature(Find.class).find(directory)) { for(Path version : session.getFeature(ListService.class).list(directory, listener).toStream() .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { @@ -141,51 +144,86 @@ public AttributedList list(final Path file, final ListProgressListener lis return versions.filter(new FilenameComparator(false)); } - /** - * @param file File to edit - * @return Directory to save previous versions of file - */ - private static Path getVersionedFolder(final Path file) { - return new Path(file.getParent(), DIRECTORY_SUFFIX, EnumSet.of(Path.Type.directory)); + @Override + public boolean isRevertable(final Path file) { + return new SimplePathPredicate(file.getParent()).test(provider.provide(file)); + } + + private interface VersioningDirectoryProvider { + /** + * @param file File to edit + * @return Directory to save previous versions of file + */ + Path provide(Path file); } - private static final char FILENAME_VERSION_SEPARATOR = '-'; - - /** - * Generate new versioned path for file - * - * @param file File - * @return Same - */ - static Path toVersioned(final Path file, final DateFormatter formatter) { - // Translate from /basename.extension to /.cyberduckversions/basename-timestamp.extension - final String basename = String.format("%s%s%s", FilenameUtils.getBaseName(file.getName()), - FILENAME_VERSION_SEPARATOR, toTimestamp(formatter)); - if(StringUtils.isNotBlank(file.getExtension())) { - return new Path(getVersionedFolder(file), String.format("%s.%s", basename, file.getExtension()), file.getType()); + public static final class DefaultVersioningDirectoryProvider implements VersioningDirectoryProvider { + private static final String NAME = ".cyberduckversions"; + + @Override + public Path provide(final Path file) { + return new Path(file.getParent(), NAME, EnumSet.of(Path.Type.directory)); } - return new Path(getVersionedFolder(file), basename, file.getType()); } - static String toTimestamp(final DateFormatter formatter) { - return formatter.format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")).replaceAll("[-:]", StringUtils.EMPTY); + public interface FilenameVersionIdentifier extends DateFormatter { + /** + * Translate from basename-timestamp.extension to /basename.extension + */ + String fromVersion(String filename); + + /** + * Translate from basename.extension to basename-timestamp.extension + */ + String toVersion(String filename); } - static Path fromVersioned(final Path file) { - // Translate from /.cyberduckersions/basename-timestamp.extension to /basename.extension - final Pattern format = Pattern.compile("(.*)-[0-9]{8}T[0-9]{6}\\.[0-9]{3}[Z](\\..*)?"); - final Matcher matcher = format.matcher(file.getName()); - if(matcher.matches()) { - if(StringUtils.isBlank(matcher.group(2))) { - return new Path(file.getParent().getParent(), matcher.group(1), file.getType()); + public static final class ISO8601FilenameVersionIdentifier implements FilenameVersionIdentifier { + private static final char FILENAME_VERSION_SEPARATOR = '-'; + + private static final ISO8601DateFormatter formatter = new ISO8601DateFormatter(); + private static final Pattern format = Pattern.compile("(.*)" + FILENAME_VERSION_SEPARATOR + "[0-9]{8}T[0-9]{6}\\.[0-9]{3}Z(\\..*)?"); + + @Override + public String format(final Date input, final TimeZone zone) { + return formatter.format(input, zone); + } + + @Override + public String format(final long milliseconds, final TimeZone zone) { + return formatter.format(milliseconds, zone); + } + + @Override + public Date parse(final String input) throws InvalidDateException { + return formatter.parse(input); + } + + @Override + public String fromVersion(final String filename) { + final Matcher matcher = format.matcher(filename); + if(matcher.matches()) { + if(StringUtils.isBlank(matcher.group(2))) { + return matcher.group(1); + } + return String.format("%s%s", matcher.group(1), matcher.group(2)); } - return new Path(file.getParent().getParent(), String.format("%s%s", matcher.group(1), matcher.group(2)), file.getType()); + return null; } - return file; - } - @Override - public boolean isRevertable(final Path file) { - return StringUtils.equals(DIRECTORY_SUFFIX, file.getParent().getName()); + @Override + public String toVersion(final String filename) { + final String basename = String.format("%s%s%s", FilenameUtils.getBaseName(filename), + FILENAME_VERSION_SEPARATOR, toTimestamp()); + if(StringUtils.isNotBlank(FilenameUtils.getExtension(filename))) { + return String.format("%s.%s", basename, FilenameUtils.getExtension(filename)); + } + return basename; + } + + private static String toTimestamp() { + return formatter.format(System.currentTimeMillis(), TimeZone.getTimeZone("UTC")) + .replaceAll("[-:]", StringUtils.EMPTY); + } } } diff --git a/core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java b/core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java deleted file mode 100644 index 19cf3abf29f..00000000000 --- a/core/src/test/java/ch/cyberduck/core/shared/DefaultVersioningFeatureTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package ch.cyberduck.core.shared; - -/* - * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. - * https://cyberduck.io/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -import ch.cyberduck.core.Path; -import ch.cyberduck.core.date.ISO8601DateFormatter; - -import org.junit.Test; - -import java.util.EnumSet; - -import static org.junit.Assert.assertEquals; - -public class DefaultVersioningFeatureTest { - - @Test - public void testToVersioned() { - { - Path f = new Path("/d/f", EnumSet.of(Path.Type.file)); - final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); - assertEquals(".cyberduckversions", versioned.getParent().getName()); - assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); - } - { - Path f = new Path("/d/f.ext", EnumSet.of(Path.Type.file)); - final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); - assertEquals(".cyberduckversions", versioned.getParent().getName()); - assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); - } - { - Path f = new Path("/d/f-f", EnumSet.of(Path.Type.file)); - final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); - assertEquals(".cyberduckversions", versioned.getParent().getName()); - assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); - } - { - Path f = new Path("/d/f-f.ext", EnumSet.of(Path.Type.file)); - final Path versioned = DefaultVersioningFeature.toVersioned(f, new ISO8601DateFormatter()); - assertEquals(".cyberduckversions", versioned.getParent().getName()); - assertEquals(f, DefaultVersioningFeature.fromVersioned(versioned)); - } - } -} \ No newline at end of file diff --git a/core/src/test/java/ch/cyberduck/core/shared/ISO8601FilenameVersionIdentifierTest.java b/core/src/test/java/ch/cyberduck/core/shared/ISO8601FilenameVersionIdentifierTest.java new file mode 100644 index 00000000000..6f90d9cd4be --- /dev/null +++ b/core/src/test/java/ch/cyberduck/core/shared/ISO8601FilenameVersionIdentifierTest.java @@ -0,0 +1,47 @@ +package ch.cyberduck.core.shared; + +/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ISO8601FilenameVersionIdentifierTest { + + @Test + public void testToVersioned() { + { + final String filename = "f"; + final String versioned = new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().toVersion(filename); + assertEquals(filename, new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().fromVersion(versioned)); + } + { + final String filename = "f.ext"; + final String versioned = new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().toVersion(filename); + assertEquals(filename, new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().fromVersion(versioned)); + } + { + final String filename = "w-f"; + final String versioned = new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().toVersion(filename); + assertEquals(filename, new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().fromVersion(versioned)); + } + { + final String filename = "w-f.ext"; + final String versioned = new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().toVersion(filename); + assertEquals(filename, new DefaultVersioningFeature.ISO8601FilenameVersionIdentifier().fromVersion(versioned)); + } + } +} \ No newline at end of file From e40d0dec7b8461e06ebde085b548a1f9cde132d9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 08:55:25 +0200 Subject: [PATCH 21/33] Merge implementations into single class and review protocol support. --- .../main/java/ch/cyberduck/core/Session.java | 12 ++-- .../shared/DefaultVersioningBulkFeature.java | 71 ------------------- .../core/shared/DefaultVersioningFeature.java | 55 +++++++++++--- .../transfer/upload/UploadFilterOptions.java | 6 +- .../cyberduck/core/worker/DeleteWorker.java | 22 +++--- 5 files changed, 61 insertions(+), 105 deletions(-) delete mode 100644 core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java diff --git a/core/src/main/java/ch/cyberduck/core/Session.java b/core/src/main/java/ch/cyberduck/core/Session.java index dcfd01e396b..53b75829086 100644 --- a/core/src/main/java/ch/cyberduck/core/Session.java +++ b/core/src/main/java/ch/cyberduck/core/Session.java @@ -45,7 +45,6 @@ import ch.cyberduck.core.shared.DefaultSearchFeature; import ch.cyberduck.core.shared.DefaultUploadFeature; import ch.cyberduck.core.shared.DefaultUrlProvider; -import ch.cyberduck.core.shared.DefaultVersioningBulkFeature; import ch.cyberduck.core.shared.DefaultVersioningFeature; import ch.cyberduck.core.shared.DelegatingHomeFeature; import ch.cyberduck.core.shared.DisabledBulkFeature; @@ -313,13 +312,6 @@ public T _getFeature(final Class type) { if(type == Download.class) { return (T) new DefaultDownloadFeature(this.getFeature(Read.class)); } - if(type == Bulk.class) { - switch(host.getProtocol().getVersioningMode()) { - case custom: - return (T) new DefaultVersioningBulkFeature(this); - } - return (T) new DisabledBulkFeature(); - } if(type == Move.class) { return (T) new DisabledMoveFeature(); } @@ -348,10 +340,14 @@ public T _getFeature(final Class type) { return (T) new DelegatingHomeFeature(new WorkdirHomeFeature(host), new DefaultPathHomeFeature(host)); } if(type == Versioning.class) { + return (T) new DefaultVersioningFeature(this); + } + if(type == Bulk.class) { switch(host.getProtocol().getVersioningMode()) { case custom: return (T) new DefaultVersioningFeature(this); } + return (T) new DisabledBulkFeature(); } return host.getProtocol().getFeature(type); } diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java deleted file mode 100644 index 90ab180c94b..00000000000 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningBulkFeature.java +++ /dev/null @@ -1,71 +0,0 @@ -package ch.cyberduck.core.shared; - -/* - * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. - * https://cyberduck.io/ - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - */ - -import ch.cyberduck.core.ConnectionCallback; -import ch.cyberduck.core.DisabledListProgressListener; -import ch.cyberduck.core.Path; -import ch.cyberduck.core.Session; -import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.features.Bulk; -import ch.cyberduck.core.features.Delete; -import ch.cyberduck.core.preferences.HostPreferences; -import ch.cyberduck.core.transfer.Transfer; -import ch.cyberduck.core.transfer.TransferItem; -import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.ui.comparator.FilenameComparator; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class DefaultVersioningBulkFeature extends DisabledBulkFeature { - private static final Logger log = LogManager.getLogger(DefaultVersioningBulkFeature.class); - - private final Session session; - private Delete delete; - - public DefaultVersioningBulkFeature(final Session session) { - this.session = session; - this.delete = session.getFeature(Delete.class); - } - - @Override - public void post(final Transfer.Type type, final Map files, final ConnectionCallback callback) throws BackgroundException { - switch(type) { - case upload: - if(new HostPreferences(session.getHost()).getBoolean("queue.upload.file.versioning")) { - for(TransferItem item : files.keySet()) { - final List versions = new DefaultVersioningFeature(session).list(item.remote, new DisabledListProgressListener()).toStream() - .sorted(new FilenameComparator(false)).skip(new HostPreferences(session.getHost()).getInteger("queue.upload.file.versioning.limit")).collect(Collectors.toList()); - if(log.isWarnEnabled()) { - log.warn(String.format("Delete %d previous versions of %s", versions.size(), item.remote)); - } - delete.delete(versions, callback, new Delete.DisabledCallback()); - } - } - } - } - - @Override - public Bulk withDelete(final Delete delete) { - this.delete = delete; - return this; - } -} diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 9912fe6916c..7775241749e 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -16,7 +16,9 @@ */ import ch.cyberduck.core.AttributedList; +import ch.cyberduck.core.ConnectionCallback; import ch.cyberduck.core.DisabledConnectionCallback; +import ch.cyberduck.core.DisabledListProgressListener; import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.PasswordCallback; @@ -29,12 +31,15 @@ import ch.cyberduck.core.date.InvalidDateException; import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.UnsupportedException; +import ch.cyberduck.core.features.Bulk; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.Transfer; +import ch.cyberduck.core.transfer.TransferItem; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.ui.comparator.FilenameComparator; @@ -45,12 +50,14 @@ import java.util.Date; import java.util.EnumSet; +import java.util.List; +import java.util.Map; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -public class DefaultVersioningFeature implements Versioning { +public class DefaultVersioningFeature extends DisabledBulkFeature implements Versioning { private static final Logger log = LogManager.getLogger(DefaultVersioningFeature.class); private final Session session; @@ -58,6 +65,8 @@ public class DefaultVersioningFeature implements Versioning { private final VersioningDirectoryProvider provider; private final Pattern include; + private Delete delete; + public DefaultVersioningFeature(final Session session) { this(session, new DefaultVersioningDirectoryProvider(), new ISO8601FilenameVersionIdentifier()); } @@ -67,11 +76,16 @@ public DefaultVersioningFeature(final Session session, final VersioningDirect this.provider = provider; this.formatter = formatter; this.include = Pattern.compile(new HostPreferences(session.getHost()).getProperty("queue.upload.file.versioning.include.regex")); + this.delete = session.getFeature(Delete.class); } @Override public VersioningConfiguration getConfiguration(final Path file) throws BackgroundException { - return new VersioningConfiguration(file.isDirectory() || include.matcher(file.getName()).matches()); + switch(session.getHost().getProtocol().getVersioningMode()) { + case custom: + return new VersioningConfiguration(file.isDirectory() || include.matcher(file.getName()).matches()); + } + return VersioningConfiguration.empty(); } @Override @@ -133,17 +147,42 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { final AttributedList versions = new AttributedList<>(); - final Path directory = provider.provide(file); - if(session.getFeature(Find.class).find(directory)) { - for(Path version : session.getFeature(ListService.class).list(directory, listener).toStream() - .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { - version.attributes().setDuplicate(true); - versions.add(version); + if(this.getConfiguration(file).isEnabled()) { + final Path directory = provider.provide(file); + if(session.getFeature(Find.class).find(directory)) { + for(Path version : session.getFeature(ListService.class).list(directory, listener).toStream() + .filter(f -> f.getName().startsWith(FilenameUtils.getBaseName(file.getName()))).collect(Collectors.toList())) { + version.attributes().setDuplicate(true); + versions.add(version); + } } } return versions.filter(new FilenameComparator(false)); } + @Override + public void post(final Transfer.Type type, final Map files, final ConnectionCallback callback) throws BackgroundException { + switch(type) { + case upload: + for(TransferItem item : files.keySet()) { + if(this.getConfiguration(item.remote).isEnabled()) { + final List versions = new DefaultVersioningFeature(session).list(item.remote, new DisabledListProgressListener()).toStream() + .sorted(new FilenameComparator(false)).skip(new HostPreferences(session.getHost()).getInteger("queue.upload.file.versioning.limit")).collect(Collectors.toList()); + if(log.isWarnEnabled()) { + log.warn(String.format("Delete %d previous versions of %s", versions.size(), item.remote)); + } + delete.delete(versions, callback, new Delete.DisabledCallback()); + } + } + } + } + + @Override + public Bulk withDelete(final Delete delete) { + this.delete = delete; + return this; + } + @Override public boolean isRevertable(final Path file) { return new SimplePathPredicate(file.getParent()).test(provider.provide(file)); diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java index 62197c17695..f65a9ca273a 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/UploadFilterOptions.java @@ -56,11 +56,7 @@ public UploadFilterOptions(final Host bookmark) { acl = preferences.getBoolean("queue.upload.acl.change"); timestamp = preferences.getBoolean("queue.upload.timestamp.change"); temporary = preferences.getBoolean("queue.upload.file.temporary"); - switch(bookmark.getProtocol().getVersioningMode()) { - case custom: - versioning = preferences.getBoolean("queue.upload.file.versioning"); - break; - } + versioning = preferences.getBoolean("queue.upload.file.versioning"); metadata = preferences.getBoolean("queue.upload.file.metadata.change"); encryption = preferences.getBoolean("queue.upload.file.encryption.change"); redundancy = preferences.getBoolean("queue.upload.file.redundancy.change"); diff --git a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java index bb7921c80ee..98951cdd5cb 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java @@ -120,21 +120,17 @@ public List run(final Session session) throws BackgroundException { if(delete.isRecursive()) { recursive.keySet().removeIf(f -> recursive.keySet().stream().anyMatch(f::isChild)); } - switch(session.getHost().getProtocol().getVersioningMode()) { - case custom: - if(new UploadFilterOptions(session.getHost()).versioning) { - final Versioning versioning = session.getFeature(Versioning.class); - for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { - final Path f = iter.next(); - if(versioning.save(f)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Skip deleting %s", f)); - } - iter.remove(); - } + if(new UploadFilterOptions(session.getHost()).versioning) { + final Versioning versioning = session.getFeature(Versioning.class); + for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { + final Path f = iter.next(); + if(versioning.save(f)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Skip deleting %s", f)); } - break; + iter.remove(); } + } } if(!recursive.isEmpty()) { delete.delete(recursive, prompt, new Delete.Callback() { From 573116d4afbc35cb735463119db46e34de6bf3bb Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 10:39:00 +0200 Subject: [PATCH 22/33] Fix revert support. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 7775241749e..0d94afb88bd 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -184,8 +184,8 @@ public Bulk withDelete(final Delete delete) { } @Override - public boolean isRevertable(final Path file) { - return new SimplePathPredicate(file.getParent()).test(provider.provide(file)); + public boolean isRevertable(final Path version) { + return StringUtils.equals(DefaultVersioningDirectoryProvider.NAME, version.getParent().getName()); } private interface VersioningDirectoryProvider { From f5dd309919cf8c45bacf22fc2521bd25422e2c43 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 10:40:27 +0200 Subject: [PATCH 23/33] Cleanup of previous folders not supported when delete feature is not recursive. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 0d94afb88bd..c87f94cb039 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -24,7 +24,6 @@ import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.Session; -import ch.cyberduck.core.SimplePathPredicate; import ch.cyberduck.core.VersioningConfiguration; import ch.cyberduck.core.date.DateFormatter; import ch.cyberduck.core.date.ISO8601DateFormatter; @@ -165,6 +164,11 @@ public void post(final Transfer.Type type, final Map versions = new DefaultVersioningFeature(session).list(item.remote, new DisabledListProgressListener()).toStream() .sorted(new FilenameComparator(false)).skip(new HostPreferences(session.getHost()).getInteger("queue.upload.file.versioning.limit")).collect(Collectors.toList()); From dd76c654ae5682f47f7b7b9f9006bb3d873dcb7f Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 10:48:16 +0200 Subject: [PATCH 24/33] Remove redundant check. --- .../ch/cyberduck/core/worker/DeleteWorker.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java index 98951cdd5cb..e2b003cf383 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java @@ -34,7 +34,6 @@ import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.preferences.PreferencesFactory; import ch.cyberduck.core.transfer.TransferStatus; -import ch.cyberduck.core.transfer.upload.UploadFilterOptions; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -120,16 +119,14 @@ public List run(final Session session) throws BackgroundException { if(delete.isRecursive()) { recursive.keySet().removeIf(f -> recursive.keySet().stream().anyMatch(f::isChild)); } - if(new UploadFilterOptions(session.getHost()).versioning) { - final Versioning versioning = session.getFeature(Versioning.class); - for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { - final Path f = iter.next(); - if(versioning.save(f)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Skip deleting %s", f)); - } - iter.remove(); + final Versioning versioning = session.getFeature(Versioning.class); + for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { + final Path f = iter.next(); + if(versioning.save(f)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Skip deleting %s", f)); } + iter.remove(); } } if(!recursive.isEmpty()) { From e936178da06e26f1c7a0dddf964d42caf09f9474 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 10:52:22 +0200 Subject: [PATCH 25/33] No versioning for previous versions it-selves. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index c87f94cb039..60f34cd99bb 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -82,6 +82,10 @@ public DefaultVersioningFeature(final Session session, final VersioningDirect public VersioningConfiguration getConfiguration(final Path file) throws BackgroundException { switch(session.getHost().getProtocol().getVersioningMode()) { case custom: + if(this.isRevertable(file)) { + // No versioning for previous versions + return VersioningConfiguration.empty(); + } return new VersioningConfiguration(file.isDirectory() || include.matcher(file.getName()).matches()); } return VersioningConfiguration.empty(); From 459749ec7bc8aa4365b00c0277c3e4710c1a6f6e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 10:54:45 +0200 Subject: [PATCH 26/33] Extract method to check for support. --- .../core/shared/DefaultVersioningFeature.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 60f34cd99bb..e3f9a5fe1e3 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -82,11 +82,7 @@ public DefaultVersioningFeature(final Session session, final VersioningDirect public VersioningConfiguration getConfiguration(final Path file) throws BackgroundException { switch(session.getHost().getProtocol().getVersioningMode()) { case custom: - if(this.isRevertable(file)) { - // No versioning for previous versions - return VersioningConfiguration.empty(); - } - return new VersioningConfiguration(file.isDirectory() || include.matcher(file.getName()).matches()); + return new VersioningConfiguration(this.isSupported(file)); } return VersioningConfiguration.empty(); } @@ -98,7 +94,7 @@ public void setConfiguration(final Path container, final PasswordCallback prompt @Override public boolean save(final Path file) throws BackgroundException { - if(this.getConfiguration(file).isEnabled()) { + if(this.isSupported(file)) { final Path version = new Path(provider.provide(file), formatter.toVersion(file.getName()), file.getType()); if(!session.getFeature(Move.class).isSupported(file, version)) { return false; @@ -150,7 +146,7 @@ public void revert(final Path file) throws BackgroundException { @Override public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { final AttributedList versions = new AttributedList<>(); - if(this.getConfiguration(file).isEnabled()) { + if(this.isSupported(file)) { final Path directory = provider.provide(file); if(session.getFeature(Find.class).find(directory)) { for(Path version : session.getFeature(ListService.class).list(directory, listener).toStream() @@ -173,7 +169,7 @@ public void post(final Transfer.Type type, final Map versions = new DefaultVersioningFeature(session).list(item.remote, new DisabledListProgressListener()).toStream() .sorted(new FilenameComparator(false)).skip(new HostPreferences(session.getHost()).getInteger("queue.upload.file.versioning.limit")).collect(Collectors.toList()); if(log.isWarnEnabled()) { @@ -196,6 +192,14 @@ public boolean isRevertable(final Path version) { return StringUtils.equals(DefaultVersioningDirectoryProvider.NAME, version.getParent().getName()); } + private boolean isSupported(final Path file) { + if(this.isRevertable(file)) { + // No versioning for previous versions + return false; + } + return file.isDirectory() || include.matcher(file.getName()).matches(); + } + private interface VersioningDirectoryProvider { /** * @param file File to edit From 31e8bcad6e85c2f68e071f9ecc7449c866217b56 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 30 Mar 2023 10:55:03 +0200 Subject: [PATCH 27/33] Move previous versions along with rename. --- .../ch/cyberduck/core/worker/MoveWorker.java | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/worker/MoveWorker.java b/core/src/main/java/ch/cyberduck/core/worker/MoveWorker.java index 988fa1a09c2..a5b2a5ddd0f 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/MoveWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/MoveWorker.java @@ -22,6 +22,7 @@ import ch.cyberduck.core.CachingAttributesFinderFeature; import ch.cyberduck.core.CachingFindFeature; import ch.cyberduck.core.ConnectionCallback; +import ch.cyberduck.core.DisabledListProgressListener; import ch.cyberduck.core.ListService; import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.MappingMimeTypeService; @@ -36,9 +37,11 @@ import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Find; import ch.cyberduck.core.features.Move; +import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.pool.SessionPool; import ch.cyberduck.core.shared.DefaultAttributesFinderFeature; import ch.cyberduck.core.shared.DefaultFindFeature; +import ch.cyberduck.core.shared.DefaultVersioningFeature; import ch.cyberduck.core.threading.BackgroundActionState; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.ui.comparator.VersionsComparator; @@ -121,18 +124,49 @@ public boolean isRunning() { if(status.isExists()) { status.withRemote(new CachingAttributesFinderFeature(cache, session.getFeature(AttributesFinder.class, new DefaultAttributesFinderFeature(session))).find(r.getValue())); } - final Path moved = feature.move(r.getKey(), r.getValue(), status, - new Delete.Callback() { - @Override - public void delete(final Path file) { - listener.message(MessageFormat.format(LocaleFactory.localizedString("Deleting {0}", "Status"), - file.getName())); - } - }, callback); + final Delete.Callback delete = new Delete.Callback() { + @Override + public void delete(final Path file) { + listener.message(MessageFormat.format(LocaleFactory.localizedString("Deleting {0}", "Status"), + file.getName())); + } + }; + final Path moved = feature.move(r.getKey(), r.getValue(), status, delete, callback); if(PathAttributes.EMPTY.equals(moved.attributes())) { moved.withAttributes(session.getFeature(AttributesFinder.class).find(moved)); } result.put(r.getKey(), moved); + // Move previous versions of file + final Versioning versioning = session.getFeature(Versioning.class); + if(versioning.getConfiguration(r.getKey()).isEnabled()) { + if(log.isDebugEnabled()) { + log.debug(String.format("List previous versions of %s", r.getKey())); + } + for(Path version : versioning.list(r.getKey(), new DisabledListProgressListener())) { + final Path target = new Path(new DefaultVersioningFeature.DefaultVersioningDirectoryProvider().provide(r.getValue()), + version.getName(), version.getType()); + final Path directory = target.getParent(); + if(!new CachingFindFeature(cache, new DefaultFindFeature(session)).find(directory)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Create directory %s for versions", directory)); + } + session.getFeature(Directory.class).mkdir(directory, new TransferStatus()); + } + if(log.isDebugEnabled()) { + log.debug(String.format("Move previous version %s to %s", version, target)); + } + if(version.isDirectory()) { + if(!session.getFeature(Move.class).isRecursive(version, target)) { + continue; + } + } + feature.move(version, target, new TransferStatus() + .withLockId(this.getLockId(version)) + .withMime(new MappingMimeTypeService().getMime(version.getName())) + .exists(new CachingFindFeature(cache, session.getFeature(Find.class, new DefaultFindFeature(session))).find(target)) + .withLength(version.attributes().getSize()), delete, callback); + } + } } } // Find previous folders to be deleted From ded1d62d8860fb5180c99f8be831cfe69273c3d1 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 2 Apr 2023 22:50:06 +0200 Subject: [PATCH 28/33] Add enabled check. --- .../java/ch/cyberduck/core/worker/DeleteWorker.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java index e2b003cf383..17094c66261 100644 --- a/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java +++ b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java @@ -122,11 +122,13 @@ public List run(final Session session) throws BackgroundException { final Versioning versioning = session.getFeature(Versioning.class); for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { final Path f = iter.next(); - if(versioning.save(f)) { - if(log.isDebugEnabled()) { - log.debug(String.format("Skip deleting %s", f)); + if(versioning.getConfiguration(f).isEnabled()) { + if(versioning.save(f)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Skip deleting %s", f)); + } + iter.remove(); } - iter.remove(); } } if(!recursive.isEmpty()) { From 90aae539798b9538a9ed47673556834e651f16cf Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 2 Apr 2023 22:50:20 +0200 Subject: [PATCH 29/33] Review. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index e3f9a5fe1e3..9dbd607b0e5 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -96,7 +96,8 @@ public void setConfiguration(final Path container, final PasswordCallback prompt public boolean save(final Path file) throws BackgroundException { if(this.isSupported(file)) { final Path version = new Path(provider.provide(file), formatter.toVersion(file.getName()), file.getType()); - if(!session.getFeature(Move.class).isSupported(file, version)) { + final Move feature = session.getFeature(Move.class); + if(!feature.isSupported(file, version)) { return false; } final Path directory = version.getParent(); @@ -110,11 +111,11 @@ public boolean save(final Path file) throws BackgroundException { log.debug(String.format("Rename existing file %s to %s", file, version)); } if(file.isDirectory()) { - if(!session.getFeature(Move.class).isRecursive(file, version)) { + if(!feature.isRecursive(file, version)) { throw new UnsupportedException(); } } - session.getFeature(Move.class).move(file, version, + feature.move(file, version, new TransferStatus().exists(false), new Delete.DisabledCallback(), new DisabledConnectionCallback()); return true; } From af2ade88ed3c19af4c38381e0824f21042be301e Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 2 Apr 2023 22:50:32 +0200 Subject: [PATCH 30/33] Check preferences. --- .../ch/cyberduck/core/shared/DefaultVersioningFeature.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 9dbd607b0e5..3d301777d63 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -198,7 +198,10 @@ private boolean isSupported(final Path file) { // No versioning for previous versions return false; } - return file.isDirectory() || include.matcher(file.getName()).matches(); + if(new HostPreferences(session.getHost()).getBoolean("queue.upload.file.versioning")) { + return file.isDirectory() || include.matcher(file.getName()).matches(); + } + return false; } private interface VersioningDirectoryProvider { From 808d18da81a9ac73911161ec85cd20711da8a4c5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Sun, 2 Apr 2023 22:54:06 +0200 Subject: [PATCH 31/33] Fix tests. --- .../ch/cyberduck/core/NullDeleteFeature.java | 28 +++++++++++++++++++ .../java/ch/cyberduck/core/NullSession.java | 4 +++ 2 files changed, 32 insertions(+) create mode 100644 core/src/test/java/ch/cyberduck/core/NullDeleteFeature.java diff --git a/core/src/test/java/ch/cyberduck/core/NullDeleteFeature.java b/core/src/test/java/ch/cyberduck/core/NullDeleteFeature.java new file mode 100644 index 00000000000..b2739685110 --- /dev/null +++ b/core/src/test/java/ch/cyberduck/core/NullDeleteFeature.java @@ -0,0 +1,28 @@ +package ch.cyberduck.core;/* + * Copyright (c) 2002-2023 iterate GmbH. All rights reserved. + * https://cyberduck.io/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Delete; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.util.Map; + +public class NullDeleteFeature implements Delete { + + @Override + public void delete(final Map files, final PasswordCallback prompt, final Callback callback) throws BackgroundException { + + } +} diff --git a/core/src/test/java/ch/cyberduck/core/NullSession.java b/core/src/test/java/ch/cyberduck/core/NullSession.java index 4fefe662221..dfb1954f9f3 100644 --- a/core/src/test/java/ch/cyberduck/core/NullSession.java +++ b/core/src/test/java/ch/cyberduck/core/NullSession.java @@ -2,6 +2,7 @@ import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.LoginCanceledException; +import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Move; import ch.cyberduck.core.features.Read; @@ -56,6 +57,9 @@ public T _getFeature(Class type) { if(type == Move.class) { return (T) new NullMoveFeature(); } + if(type == Delete.class) { + return (T) new NullDeleteFeature(); + } if(type == Directory.class) { return (T) new NullDirectoryFeature(); } From 2746b8924fa5d03c04c5eba5047a07c0562ff4e5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 6 Sep 2023 09:09:22 +0200 Subject: [PATCH 32/33] Make interface public. --- .../java/ch/cyberduck/core/shared/DefaultVersioningFeature.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java index 3d301777d63..3b545bb5926 100644 --- a/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -204,7 +204,7 @@ private boolean isSupported(final Path file) { return false; } - private interface VersioningDirectoryProvider { + public interface VersioningDirectoryProvider { /** * @param file File to edit * @return Directory to save previous versions of file From e8f47e38d30a0bcfb07e1339e63f04db26715ba9 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Wed, 6 Sep 2023 15:22:27 +0200 Subject: [PATCH 33/33] Do not apply versioning when resuming upload. --- .../ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java index 9cf0b6b1454..558f11d10c1 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/AbstractUploadFilter.java @@ -308,7 +308,7 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer @Override public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener listener) throws BackgroundException { - if(status.isExists()) { + if(status.isExists() && !status.isAppend()) { if(options.versioning) { final Versioning feature = session.getFeature(Versioning.class); if(feature.save(file)) {