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/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/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 2ea91ec2b66..b12ef5bf54a 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. */ @@ -352,6 +354,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/core/src/main/java/ch/cyberduck/core/Session.java b/core/src/main/java/ch/cyberduck/core/Session.java index ab452eb1656..53b75829086 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; @@ -99,7 +101,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) { @@ -310,9 +312,6 @@ public T _getFeature(final Class type) { if(type == Download.class) { return (T) new DefaultDownloadFeature(this.getFeature(Read.class)); } - if(type == Bulk.class) { - return (T) new DisabledBulkFeature(); - } if(type == Move.class) { return (T) new DisabledMoveFeature(); } @@ -340,6 +339,16 @@ public T _getFeature(final Class type) { if(type == Home.class) { 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/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/main/java/ch/cyberduck/core/features/Versioning.java b/core/src/main/java/ch/cyberduck/core/features/Versioning.java index b7ea5b1beb9..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 * @@ -58,7 +68,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(); } 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..3b545bb5926 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/shared/DefaultVersioningFeature.java @@ -0,0 +1,284 @@ +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.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; +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.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; + +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.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 extends DisabledBulkFeature implements Versioning { + private static final Logger log = LogManager.getLogger(DefaultVersioningFeature.class); + + private final Session session; + private final FilenameVersionIdentifier formatter; + private final VersioningDirectoryProvider provider; + private final Pattern include; + + private Delete delete; + + public DefaultVersioningFeature(final Session session) { + this(session, new DefaultVersioningDirectoryProvider(), new ISO8601FilenameVersionIdentifier()); + } + + 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")); + this.delete = session.getFeature(Delete.class); + } + + @Override + public VersioningConfiguration getConfiguration(final Path file) throws BackgroundException { + switch(session.getHost().getProtocol().getVersioningMode()) { + case custom: + return new VersioningConfiguration(this.isSupported(file)); + } + return VersioningConfiguration.empty(); + } + + @Override + public void setConfiguration(final Path container, final PasswordCallback prompt, final VersioningConfiguration configuration) throws BackgroundException { + throw new UnsupportedException(); + } + + @Override + 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()); + final Move feature = session.getFeature(Move.class); + if(!feature.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)); + } + if(file.isDirectory()) { + if(!feature.isRecursive(file, version)) { + throw new UnsupportedException(); + } + } + feature.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 { + 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)) { + 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 + public AttributedList list(final Path file, final ListProgressListener listener) throws BackgroundException { + final AttributedList versions = new AttributedList<>(); + 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() + .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(item.remote.isDirectory()) { + if(!delete.isRecursive()) { + continue; + } + } + if(this.isSupported(item.remote)) { + 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 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; + } + if(new HostPreferences(session.getHost()).getBoolean("queue.upload.file.versioning")) { + return file.isDirectory() || include.matcher(file.getName()).matches(); + } + return false; + } + + public interface VersioningDirectoryProvider { + /** + * @param file File to edit + * @return Directory to save previous versions of file + */ + Path provide(Path file); + } + + 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)); + } + } + + 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); + } + + 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 null; + } + + @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/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/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..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 @@ -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; @@ -64,7 +65,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; @@ -306,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() && !status.isAppend()) { + 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 cfb9dbe74dc..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 @@ -32,6 +32,10 @@ public final class UploadFilterOptions { * Create temporary filename with an UUID and rename when upload is complete */ public boolean temporary; + /** + * Move existing file to versioning directory + */ + public boolean versioning; /** * Enable server side encryption if available */ @@ -52,6 +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"); + 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/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/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java b/core/src/main/java/ch/cyberduck/core/worker/DeleteWorker.java index 091d4ffee53..17094c66261 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,6 +31,7 @@ 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.TransferStatus; @@ -41,6 +41,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 +113,42 @@ 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); + final Versioning versioning = session.getFeature(Versioning.class); + for(Iterator iter = recursive.keySet().iterator(); iter.hasNext(); ) { + final Path f = iter.next(); + if(versioning.getConfiguration(f).isEnabled()) { + if(versioning.save(f)) { + if(log.isDebugEnabled()) { + log.debug(String.format("Skip deleting %s", f)); } + iter.remove(); } } - }); + } + 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 +176,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 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 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/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(); } 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/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 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)); 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/defaults/src/main/resources/default.properties b/defaults/src/main/resources/default.properties index c365baf2e62..1b5b62567a8 100644 --- a/defaults/src/main/resources/default.properties +++ b/defaults/src/main/resources/default.properties @@ -180,6 +180,10 @@ 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.upload.file.versioning.include.regex=.* +# 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 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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)); 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/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) { 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; + } }