From 975618f1e4d5f82d03b434a7c583d917242a3ed5 Mon Sep 17 00:00:00 2001 From: David Kocher Date: Thu, 14 Nov 2024 13:17:23 +0100 Subject: [PATCH] Extract filters for protocol features used for specific file attributes in file transfers. --- .../core/transfer/ChainedFeatureFilter.java | 53 +++ .../core/transfer/FeatureFilter.java | 46 +++ .../download/AbstractDownloadFilter.java | 292 +--------------- .../features/ChecksumFeatureFilter.java | 69 ++++ .../DefaultDownloadOptionsFilterChain.java | 35 ++ .../download/features/IconFilter.java | 43 +++ .../download/features/LauncherFilter.java | 39 +++ .../features/PermissionFeatureFilter.java | 85 +++++ .../download/features/QuarantineFilter.java | 67 ++++ .../features/SegmentedFeatureFilter.java | 164 +++++++++ .../features/TemporaryFeatureFilter.java | 68 ++++ .../features/TimestampFeatureFilter.java | 54 +++ .../transfer/upload/AbstractUploadFilter.java | 316 ++---------------- .../upload/features/AclFeatureFilter.java | 86 +++++ .../features/ChecksumFeatureFilter.java | 66 ++++ .../DefaultLocalUploadOptionsFilterChain.java | 39 +++ .../features/EncryptionFeatureFilter.java | 62 ++++ .../upload/features/HiddenFeatureFilter.java | 46 +++ .../features/MetadataFeatureFilter.java | 62 ++++ .../upload/features/MimeFeatureFilter.java | 48 +++ .../features/PermissionFeatureFilter.java | 88 +++++ .../RedundancyClassFeatureFilter.java | 64 ++++ .../features/TemporaryFeatureFilter.java | 88 +++++ .../features/TimestampFeatureFilter.java | 77 +++++ .../features/VersioningFeatureFilter.java | 55 +++ .../download/AbstractDownloadFilterTest.java | 34 +- 26 files changed, 1573 insertions(+), 573 deletions(-) create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/ChainedFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/FeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/ChecksumFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/DefaultDownloadOptionsFilterChain.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/IconFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/LauncherFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/PermissionFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/QuarantineFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/SegmentedFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/TemporaryFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/download/features/TimestampFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/AclFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/ChecksumFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/DefaultLocalUploadOptionsFilterChain.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/EncryptionFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/HiddenFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/MetadataFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/MimeFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/PermissionFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/RedundancyClassFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/TemporaryFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/TimestampFeatureFilter.java create mode 100644 core/src/main/java/ch/cyberduck/core/transfer/upload/features/VersioningFeatureFilter.java diff --git a/core/src/main/java/ch/cyberduck/core/transfer/ChainedFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/ChainedFeatureFilter.java new file mode 100644 index 00000000000..bd49d967f49 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/ChainedFeatureFilter.java @@ -0,0 +1,53 @@ +package ch.cyberduck.core.transfer; + +/* + * Copyright (c) 2002-2024 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.exception.BackgroundException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +public class ChainedFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(ChainedFeatureFilter.class); + + private final FeatureFilter[] filters; + + public ChainedFeatureFilter(final FeatureFilter... filters) { + this.filters = filters; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + for(final FeatureFilter filter : filters) { + log.debug("Prepare {} with {}", file, filter); + filter.prepare(file, local, status, progress); + } + return status; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + for(final FeatureFilter filter : filters) { + log.debug("Complete {} with {}", file, filter); + filter.complete(file, local, status, progress); + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/FeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/FeatureFilter.java new file mode 100644 index 00000000000..543477e56eb --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/FeatureFilter.java @@ -0,0 +1,46 @@ +package ch.cyberduck.core.transfer; + +/* + * Copyright (c) 2002-2024 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.exception.BackgroundException; + +import java.util.Optional; + +public interface FeatureFilter { + default TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + // No-op + return status; + } + + default void apply(final Path file, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + // No-op + } + + default void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + // No-op + } + + FeatureFilter noop = new FeatureFilter() { + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) { + // No-op + return status; + } + }; +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilter.java index ef5fd2404dd..986edccb125 100644 --- a/core/src/main/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilter.java +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilter.java @@ -17,77 +17,44 @@ * Bug fixes, suggestions and comments should be sent to feedback@cyberduck.ch */ -import ch.cyberduck.core.DescriptiveUrl; -import ch.cyberduck.core.DescriptiveUrlBag; -import ch.cyberduck.core.HostUrlProvider; import ch.cyberduck.core.Local; -import ch.cyberduck.core.LocalFactory; -import ch.cyberduck.core.LocaleFactory; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; -import ch.cyberduck.core.Permission; import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.Session; -import ch.cyberduck.core.UrlProvider; -import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.exception.ChecksumException; import ch.cyberduck.core.exception.LocalAccessDeniedException; import ch.cyberduck.core.exception.NotfoundException; import ch.cyberduck.core.features.AttributesFinder; -import ch.cyberduck.core.features.Read; -import ch.cyberduck.core.io.Checksum; -import ch.cyberduck.core.io.ChecksumCompute; -import ch.cyberduck.core.io.ChecksumComputeFactory; import ch.cyberduck.core.local.ApplicationLauncher; import ch.cyberduck.core.local.ApplicationLauncherFactory; -import ch.cyberduck.core.local.IconService; -import ch.cyberduck.core.local.IconServiceFactory; -import ch.cyberduck.core.local.QuarantineService; -import ch.cyberduck.core.local.QuarantineServiceFactory; -import ch.cyberduck.core.preferences.HostPreferences; -import ch.cyberduck.core.preferences.PreferencesReader; -import ch.cyberduck.core.transfer.AutoTransferConnectionLimiter; +import ch.cyberduck.core.transfer.FeatureFilter; import ch.cyberduck.core.transfer.TransferPathFilter; import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.core.transfer.download.features.DefaultDownloadOptionsFilterChain; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; -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.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import java.util.Optional; public abstract class AbstractDownloadFilter implements TransferPathFilter { private static final Logger log = LogManager.getLogger(AbstractDownloadFilter.class); - private final PreferencesReader preferences; - private final Session session; - private final SymlinkResolver symlinkResolver; - private final QuarantineService quarantine = QuarantineServiceFactory.get(); private final ApplicationLauncher launcher = ApplicationLauncherFactory.get(); - private final IconService icon = IconServiceFactory.get(); - + private final SymlinkResolver resolver; private final AttributesFinder attribute; - private final DownloadFilterOptions options; + private final FeatureFilter chain; - protected AbstractDownloadFilter(final SymlinkResolver symlinkResolver, final Session session, final DownloadFilterOptions options) { - this(symlinkResolver, session, session.getFeature(AttributesFinder.class), options); + protected AbstractDownloadFilter(final SymlinkResolver resolver, final Session session, final DownloadFilterOptions options) { + this(resolver, session, session.getFeature(AttributesFinder.class), options); } - public AbstractDownloadFilter(final SymlinkResolver symlinkResolver, final Session session, final AttributesFinder attribute, final DownloadFilterOptions options) { - this.session = session; - this.symlinkResolver = symlinkResolver; + public AbstractDownloadFilter(final SymlinkResolver resolver, final Session session, final AttributesFinder attribute, final DownloadFilterOptions options) { + this.resolver = resolver; this.attribute = attribute; - this.options = options; - this.preferences = new HostPreferences(session.getHost()); + this.chain = new DefaultDownloadOptionsFilterChain(session, options); } @Override @@ -124,7 +91,7 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer final Path target = file.getSymlinkTarget(); // Read remote attributes of symlink target attributes = attribute.find(target); - if(!symlinkResolver.resolve(file)) { + if(!resolver.resolve(file)) { if(file.isFile()) { // Content length status.setLength(attributes.getSize()); @@ -138,260 +105,33 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer if(file.isFile()) { // Content length status.setLength(attributes.getSize()); - if(StringUtils.startsWith(attributes.getDisplayname(), "file:")) { - final String filename = StringUtils.removeStart(attributes.getDisplayname(), "file:"); - if(!StringUtils.equals(file.getName(), filename)) { - status.withDisplayname(LocalFactory.get(local.getParent(), filename)); - int no = 0; - while(status.getDisplayname().local.exists()) { - String proposal = String.format("%s-%d", FilenameUtils.getBaseName(filename), ++no); - if(StringUtils.isNotBlank(Path.getExtension(filename))) { - proposal += String.format(".%s", Path.getExtension(filename)); - } - status.withDisplayname(LocalFactory.get(local.getParent(), proposal)); - } - } - } } } status.setRemote(attributes); - if(options.timestamp) { - status.setModified(attributes.getModificationDate()); - } - if(options.permissions) { - Permission permission = Permission.EMPTY; - if(preferences.getBoolean("queue.download.permissions.default")) { - if(file.isFile()) { - permission = new Permission( - preferences.getInteger("queue.download.permissions.file.default")); - } - if(file.isDirectory()) { - permission = new Permission( - preferences.getInteger("queue.download.permissions.folder.default")); - } - } - else { - permission = attributes.getPermission(); - } - status.setPermission(permission); - } - status.setAcl(attributes.getAcl()); - if(options.segments) { - if(!session.getFeature(Read.class).offset(file)) { - log.warn("Reading with offsets not supported for {}", file); - } - else { - if(file.isFile()) { - // Free space on disk - long space = 0L; - try { - space = Files.getFileStore(Paths.get(local.getParent().getAbsolute())).getUsableSpace(); - } - catch(IOException e) { - log.warn("Failure to determine disk space for {}", file.getParent()); - } - long threshold = preferences.getLong("queue.download.segments.threshold"); - if(status.getLength() * 2 > space) { - log.warn("Insufficient free disk space {} for segmented download of {}", space, file); - } - else if(status.getLength() > threshold) { - // if file is smaller than threshold do not attempt to segment - final long segmentSize = findSegmentSize(status.getLength(), - new AutoTransferConnectionLimiter().getLimit(session.getHost()), threshold, - preferences.getLong("queue.download.segments.size"), - preferences.getLong("queue.download.segments.count")); - - // with default settings this can handle files up to 16 GiB, with 128 segments at 128 MiB. - // this scales down to files of size 20MiB with 2 segments at 10 MiB - long remaining = status.getLength(), offset = 0; - // Sorted list - final List segments = new ArrayList<>(); - final Local segmentsFolder = LocalFactory.get(local.getParent(), String.format("%s.cyberducksegment", local.getName())); - for(int segmentNumber = 1; remaining > 0; segmentNumber++) { - final Local segmentFile = LocalFactory.get( - segmentsFolder, String.format("%d.cyberducksegment", segmentNumber)); - // Last part can be less than 5 MB. Adjust part size. - long length = Math.min(segmentSize, remaining); - final TransferStatus segmentStatus = new TransferStatus() - .segment(true) // Skip completion filter for single segment - .append(true) // Read with offset - .withOffset(offset) - .withLength(length) - .withRename(segmentFile); - log.debug("Adding status {} for segment {}", segmentStatus, segmentFile); - segments.add(segmentStatus); - remaining -= length; - offset += length; - } - status.withSegments(segments); - } - } - } - } - if(options.checksum) { - status.setChecksum(attributes.getChecksum()); - } - return status; + return chain.prepare(file, Optional.of(local), status, progress); } @Override - public void apply(final Path file, final Local local, final TransferStatus status, - final ProgressListener listener) throws BackgroundException { - // + public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + chain.apply(file, status, progress); } /** * Update timestamp and permission */ @Override - public void complete(final Path file, final Local local, - final TransferStatus status, final ProgressListener listener) throws BackgroundException { + public void complete(final Path file, final Local local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { log.debug("Complete {} with status {}", file.getAbsolute(), status); if(status.isSegment()) { log.debug("Skip completion for single segment {}", status); return; } if(status.isComplete()) { - if(status.isSegmented()) { - // Obtain ordered list of segments to reassemble - final List segments = status.getSegments(); - log.info("Compile {} segments to file {}", segments.size(), local); - if(local.exists()) { - local.delete(); - } - for(Iterator iterator = segments.iterator(); iterator.hasNext(); ) { - final TransferStatus segmentStatus = iterator.next(); - // Segment - final Local segmentFile = segmentStatus.getRename().local; - log.info("Append segment {} to {}", segmentFile, local); - segmentFile.copy(local, new Local.CopyOptions().append(true)); - log.info("Delete segment {}", segmentFile); - segmentFile.delete(); - if(!iterator.hasNext()) { - final Local folder = segmentFile.getParent(); - log.info("Remove segment folder {}", folder); - folder.delete(); - } - } - } - log.debug("Run completion for file {} with status {}", local, status); + chain.complete(file, Optional.of(local), status, progress); if(file.isFile()) { // Bounce Downloads folder dock icon by sending download finished notification launcher.bounce(local); - // Remove custom icon if complete. The Finder will display the default icon for this file type - if(options.icon) { - icon.set(local, status); - icon.remove(local); - } - if(options.quarantine || options.wherefrom) { - final DescriptiveUrlBag provider = session.getFeature(UrlProvider.class).toUrl(file).filter(DescriptiveUrl.Type.provider, DescriptiveUrl.Type.http); - for(DescriptiveUrl url : provider) { - try { - if(options.quarantine) { - // Set quarantine attributes - quarantine.setQuarantine(local, new HostUrlProvider().withUsername(false).get(session.getHost()), url.getUrl()); - } - if(options.wherefrom) { - // Set quarantine attributes - quarantine.setWhereFrom(local, url.getUrl()); - } - } - catch(LocalAccessDeniedException e) { - log.warn("Failure to quarantine file {}. {}", file, e.getMessage()); - } - break; - } - } - } - if(!Permission.EMPTY.equals(status.getPermission())) { - if(file.isDirectory()) { - // Make sure we can read & write files to directory created. - status.getPermission().setUser(status.getPermission().getUser().or(Permission.Action.read).or(Permission.Action.write).or(Permission.Action.execute)); - } - if(file.isFile()) { - // Make sure the owner can always read and write. - status.getPermission().setUser(status.getPermission().getUser().or(Permission.Action.read).or(Permission.Action.write)); - } - log.info("Updating permissions of {} to {}", local, status.getPermission()); - try { - local.attributes().setPermission(status.getPermission()); - } - catch(AccessDeniedException e) { - // Ignore - log.warn(e.getMessage()); - } - } - if(status.getModified() != null) { - log.info("Updating timestamp of {} to {}", local, status.getModified()); - try { - local.attributes().setModificationDate(status.getModified()); - } - catch(AccessDeniedException e) { - // Ignore - log.warn(e.getMessage()); - } - } - if(file.isFile()) { - if(options.checksum) { - if(file.getType().contains(Path.Type.decrypted)) { - log.warn("Skip checksum verification for {} with client side encryption enabled", file); - } - else { - final Checksum checksum = status.getChecksum(); - if(Checksum.NONE != checksum) { - final ChecksumCompute compute = ChecksumComputeFactory.get(checksum.algorithm); - listener.message(MessageFormat.format(LocaleFactory.localizedString("Calculate checksum for {0}", "Status"), - file.getName())); - final Checksum download = compute.compute(local.getInputStream(), new TransferStatus()); - if(!checksum.equals(download)) { - throw new ChecksumException( - MessageFormat.format(LocaleFactory.localizedString("Download {0} failed", "Error"), file.getName()), - MessageFormat.format(LocaleFactory.localizedString("Mismatch between {0} hash {1} of downloaded data and checksum {2} returned by the server", "Error"), - download.algorithm.toString(), download.hash, checksum.hash)); - } - } - } - } - } - if(file.isFile()) { - if(status.getDisplayname().local != null) { - log.info("Rename file {} to {}", file, status.getDisplayname().local); - local.rename(status.getDisplayname().local); - } - if(options.open) { - launcher.open(local); - } - } - } - } - - static long findSegmentSize(final long length, final int initialSplit, final long segmentThreshold, final long segmentSizeMaximum, final long segmentCountLimit) { - // Make segments - long parts, segmentSize, nextParts = initialSplit; - // find segment size - // starting with part count of queue.connections.limit - // but not more than queue.download.segments.count - // or until smaller than queue.download.segments.threshold - do { - parts = nextParts; - nextParts = Math.min(nextParts * 2, segmentCountLimit); - // round up to next byte - segmentSize = (length + 1) / parts; - } - while(segmentSize > segmentThreshold && parts < segmentCountLimit); - // round to next divisible by 2 - segmentSize = (segmentSize * 2 + 1) / 2; - // if larger than maximum segment size - if(segmentSize > segmentSizeMaximum) { - // double segment size until parts smaller than queue.download.segments.count - long nextSize = segmentSizeMaximum; - do { - segmentSize = nextSize; - nextSize *= 2; - parts = length / segmentSize; } - while(parts > segmentCountLimit); } - return segmentSize; } } diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/ChecksumFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/ChecksumFeatureFilter.java new file mode 100644 index 00000000000..449283d936b --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/ChecksumFeatureFilter.java @@ -0,0 +1,69 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.ChecksumException; +import ch.cyberduck.core.io.Checksum; +import ch.cyberduck.core.io.ChecksumCompute; +import ch.cyberduck.core.io.ChecksumComputeFactory; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Optional; + +public class ChecksumFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(ChecksumFeatureFilter.class); + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + return status.withChecksum(status.getRemote().getChecksum()); + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + if(file.getType().contains(Path.Type.decrypted)) { + log.warn("Skip checksum verification for {} with client side encryption enabled", file); + } + else { + final Checksum checksum = status.getChecksum(); + if(Checksum.NONE != checksum) { + if(local.isPresent()) { + final ChecksumCompute compute = ChecksumComputeFactory.get(checksum.algorithm); + progress.message(MessageFormat.format(LocaleFactory.localizedString("Calculate checksum for {0}", "Status"), + file.getName())); + final Checksum download = compute.compute(local.get().getInputStream(), new TransferStatus()); + if(!checksum.equals(download)) { + throw new ChecksumException( + MessageFormat.format(LocaleFactory.localizedString("Download {0} failed", "Error"), file.getName()), + MessageFormat.format(LocaleFactory.localizedString("Mismatch between {0} hash {1} of downloaded data and checksum {2} returned by the server", "Error"), + download.algorithm.toString(), download.hash, checksum.hash)); + } + } + } + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/DefaultDownloadOptionsFilterChain.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/DefaultDownloadOptionsFilterChain.java new file mode 100644 index 00000000000..18406305d59 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/DefaultDownloadOptionsFilterChain.java @@ -0,0 +1,35 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.Session; +import ch.cyberduck.core.transfer.ChainedFeatureFilter; +import ch.cyberduck.core.transfer.download.DownloadFilterOptions; + +public class DefaultDownloadOptionsFilterChain extends ChainedFeatureFilter { + + public DefaultDownloadOptionsFilterChain(final Session session, final DownloadFilterOptions options) { + super( + options.timestamp ? new TimestampFeatureFilter() : noop, + options.permissions ? new PermissionFeatureFilter(session) : noop, + options.checksum ? new ChecksumFeatureFilter() : noop, + new TemporaryFeatureFilter(), + options.icon ? new IconFilter() : noop, + options.quarantine ? new QuarantineFilter(session) : noop, + options.open ? new LauncherFilter() : noop + ); + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/IconFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/IconFilter.java new file mode 100644 index 00000000000..149fa494712 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/IconFilter.java @@ -0,0 +1,43 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.exception.BackgroundException; +import ch.cyberduck.core.local.IconService; +import ch.cyberduck.core.local.IconServiceFactory; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.util.Optional; + +public class IconFilter implements FeatureFilter { + + private final IconService icon = IconServiceFactory.get(); + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + // Remove custom icon if complete. The Finder will display the default icon for this file type + if(local.isPresent()) { + icon.set(local.get(), status); + icon.remove(local.get()); + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/LauncherFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/LauncherFilter.java new file mode 100644 index 00000000000..cdfbcc84e7c --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/LauncherFilter.java @@ -0,0 +1,39 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.exception.BackgroundException; +import ch.cyberduck.core.local.ApplicationLauncher; +import ch.cyberduck.core.local.ApplicationLauncherFactory; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.util.Optional; + +public class LauncherFilter implements FeatureFilter { + + private final ApplicationLauncher launcher = ApplicationLauncherFactory.get(); + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + local.ifPresent(launcher::open); + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/PermissionFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/PermissionFeatureFilter.java new file mode 100644 index 00000000000..0dc4026ac22 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/PermissionFeatureFilter.java @@ -0,0 +1,85 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.Permission; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.AccessDeniedException; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +public class PermissionFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(PermissionFeatureFilter.class); + + private final Session session; + + public PermissionFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + Permission permission = Permission.EMPTY; + if(new HostPreferences(session.getHost()).getBoolean("queue.download.permissions.default")) { + if(file.isFile()) { + permission = new Permission( + new HostPreferences(session.getHost()).getInteger("queue.download.permissions.file.default")); + } + if(file.isDirectory()) { + permission = new Permission( + new HostPreferences(session.getHost()).getInteger("queue.download.permissions.folder.default")); + } + } + else { + permission = status.getRemote().getPermission(); + } + return status.withPermission(permission); + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(!Permission.EMPTY.equals(status.getPermission())) { + if(file.isDirectory()) { + // Make sure we can read & write files to directory created. + status.getPermission().setUser(status.getPermission().getUser().or(Permission.Action.read).or(Permission.Action.write).or(Permission.Action.execute)); + } + if(file.isFile()) { + // Make sure the owner can always read and write. + status.getPermission().setUser(status.getPermission().getUser().or(Permission.Action.read).or(Permission.Action.write)); + } + if(local.isPresent()) { + log.info("Updating permissions of {} to {}", local, status.getPermission()); + try { + local.get().attributes().setPermission(status.getPermission()); + } + catch(AccessDeniedException e) { + // Ignore + log.warn(e.getMessage()); + } + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/QuarantineFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/QuarantineFilter.java new file mode 100644 index 00000000000..97e69ab7c14 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/QuarantineFilter.java @@ -0,0 +1,67 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.DescriptiveUrl; +import ch.cyberduck.core.DescriptiveUrlBag; +import ch.cyberduck.core.HostUrlProvider; +import ch.cyberduck.core.Local; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.UrlProvider; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.LocalAccessDeniedException; +import ch.cyberduck.core.local.QuarantineService; +import ch.cyberduck.core.local.QuarantineServiceFactory; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +public class QuarantineFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(QuarantineFilter.class); + + private final QuarantineService quarantine = QuarantineServiceFactory.get(); + + private final Session session; + + public QuarantineFilter(final Session session) { + this.session = session; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(local.isPresent()) { + final DescriptiveUrlBag provider = session.getFeature(UrlProvider.class).toUrl(file).filter(DescriptiveUrl.Type.provider, DescriptiveUrl.Type.http); + for(DescriptiveUrl url : provider) { + try { + // Set quarantine attributes + quarantine.setQuarantine(local.get(), new HostUrlProvider().withUsername(false).get(session.getHost()), url.getUrl()); + // Set quarantine attributes + quarantine.setWhereFrom(local.get(), url.getUrl()); + } + catch(LocalAccessDeniedException e) { + log.warn("Failure to quarantine file {}. {}", file, e.getMessage()); + } + break; + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/SegmentedFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/SegmentedFeatureFilter.java new file mode 100644 index 00000000000..2c6d87bf77f --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/SegmentedFeatureFilter.java @@ -0,0 +1,164 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.LocalFactory; +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.Read; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.AutoTransferConnectionLimiter; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +public class SegmentedFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(SegmentedFeatureFilter.class); + + private final Session session; + + public SegmentedFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(!session.getFeature(Read.class).offset(file)) { + log.warn("Reading with offsets not supported for {}", file); + } + else { + if(file.isFile()) { + if(local.isPresent()) { + // Free space on disk + long space = 0L; + try { + space = Files.getFileStore(Paths.get(local.get().getParent().getAbsolute())).getUsableSpace(); + } + catch(IOException e) { + log.warn("Failure to determine disk space for {}", file.getParent()); + } + long threshold = new HostPreferences(session.getHost()).getLong("queue.download.segments.threshold"); + if(status.getLength() * 2 > space) { + log.warn("Insufficient free disk space {} for segmented download of {}", space, file); + } + else if(status.getLength() > threshold) { + // if file is smaller than threshold do not attempt to segment + final long segmentSize = findSegmentSize(status.getLength(), + new AutoTransferConnectionLimiter().getLimit(session.getHost()), threshold, + new HostPreferences(session.getHost()).getLong("queue.download.segments.size"), + new HostPreferences(session.getHost()).getLong("queue.download.segments.count")); + + // with default settings this can handle files up to 16 GiB, with 128 segments at 128 MiB. + // this scales down to files of size 20MiB with 2 segments at 10 MiB + long remaining = status.getLength(), offset = 0; + // Sorted list + final List segments = new ArrayList<>(); + final Local segmentsFolder = LocalFactory.get(local.get().getParent(), String.format("%s.cyberducksegment", local.get().getName())); + for(int segmentNumber = 1; remaining > 0; segmentNumber++) { + final Local segmentFile = LocalFactory.get( + segmentsFolder, String.format("%d.cyberducksegment", segmentNumber)); + // Last part can be less than 5 MB. Adjust part size. + long length = Math.min(segmentSize, remaining); + final TransferStatus segmentStatus = new TransferStatus() + .segment(true) // Skip completion filter for single segment + .append(true) // Read with offset + .withOffset(offset) + .withLength(length) + .withRename(segmentFile); + log.debug("Adding status {} for segment {}", segmentStatus, segmentFile); + segments.add(segmentStatus); + remaining -= length; + offset += length; + } + status.withSegments(segments); + } + } + } + } + return status; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(status.isSegmented()) { + if(local.isPresent()) { + // Obtain ordered list of segments to reassemble + final List segments = status.getSegments(); + log.info("Compile {} segments to file {}", segments.size(), local); + if(local.get().exists()) { + local.get().delete(); + } + for(Iterator iterator = segments.iterator(); iterator.hasNext(); ) { + final TransferStatus segmentStatus = iterator.next(); + // Segment + final Local segmentFile = segmentStatus.getRename().local; + log.info("Append segment {} to {}", segmentFile, local); + segmentFile.copy(local.get(), new Local.CopyOptions().append(true)); + log.info("Delete segment {}", segmentFile); + segmentFile.delete(); + if(!iterator.hasNext()) { + final Local folder = segmentFile.getParent(); + log.info("Remove segment folder {}", folder); + folder.delete(); + } + } + } + } + } + + public static long findSegmentSize(final long length, final int initialSplit, final long segmentThreshold, final long segmentSizeMaximum, final long segmentCountLimit) { + // Make segments + long parts, segmentSize, nextParts = initialSplit; + // find segment size + // starting with part count of queue.connections.limit + // but not more than queue.download.segments.count + // or until smaller than queue.download.segments.threshold + do { + parts = nextParts; + nextParts = Math.min(nextParts * 2, segmentCountLimit); + // round up to next byte + segmentSize = (length + 1) / parts; + } + while(segmentSize > segmentThreshold && parts < segmentCountLimit); + // round to next divisible by 2 + segmentSize = (segmentSize * 2 + 1) / 2; + // if larger than maximum segment size + if(segmentSize > segmentSizeMaximum) { + // double segment size until parts smaller than queue.download.segments.count + long nextSize = segmentSizeMaximum; + do { + segmentSize = nextSize; + nextSize *= 2; + parts = length / segmentSize; + } + while(parts > segmentCountLimit); + } + return segmentSize; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/TemporaryFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/TemporaryFeatureFilter.java new file mode 100644 index 00000000000..2792bbf785b --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/TemporaryFeatureFilter.java @@ -0,0 +1,68 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.LocalFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +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.Optional; + +public class TemporaryFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(TemporaryFeatureFilter.class); + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(local.isPresent()) { + if(StringUtils.startsWith(status.getRemote().getDisplayname(), "file:")) { + final String filename = StringUtils.removeStart(status.getRemote().getDisplayname(), "file:"); + if(!StringUtils.equals(file.getName(), filename)) { + status.withDisplayname(LocalFactory.get(local.get().getParent(), filename)); + int no = 0; + while(status.getDisplayname().local.exists()) { + String proposal = String.format("%s-%d", FilenameUtils.getBaseName(filename), ++no); + if(StringUtils.isNotBlank(Path.getExtension(filename))) { + proposal += String.format(".%s", Path.getExtension(filename)); + } + status.withDisplayname(LocalFactory.get(local.get().getParent(), proposal)); + } + } + } + } + return status; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + if(status.getDisplayname().local != null) { + if(local.isPresent()) { + log.info("Rename file {} to {}", file, status.getDisplayname().local); + local.get().rename(status.getDisplayname().local); + } + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/download/features/TimestampFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/download/features/TimestampFeatureFilter.java new file mode 100644 index 00000000000..eed4eee8d5e --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/download/features/TimestampFeatureFilter.java @@ -0,0 +1,54 @@ +package ch.cyberduck.core.transfer.download.features; + +/* + * Copyright (c) 2002-2024 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.exception.AccessDeniedException; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Optional; + +public class TimestampFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(TimestampFeatureFilter.class); + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + return status.withModified(status.getRemote().getModificationDate()).withCreated(status.getRemote().getCreationDate()); + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(status.getModified() != null) { + if(local.isPresent()) { + log.info("Updating timestamp of {} to {}", local, status.getModified()); + try { + local.get().attributes().setModificationDate(status.getModified()); + } + catch(AccessDeniedException e) { + // Ignore + log.warn(e.getMessage()); + } + } + } + } +} 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 fbd0480adc1..7cf509ee5a8 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 @@ -17,75 +17,48 @@ * Bug fixes, suggestions and comments should be sent to feedback@cyberduck.ch */ -import ch.cyberduck.core.Acl; -import ch.cyberduck.core.AlphanumericRandomStringService; -import ch.cyberduck.core.DisabledConnectionCallback; -import ch.cyberduck.core.Filter; import ch.cyberduck.core.Local; -import ch.cyberduck.core.LocaleFactory; -import ch.cyberduck.core.MappingMimeTypeService; import ch.cyberduck.core.Path; -import ch.cyberduck.core.PathAttributes; -import ch.cyberduck.core.Permission; import ch.cyberduck.core.ProgressListener; import ch.cyberduck.core.Session; -import ch.cyberduck.core.UserDateFormatterFactory; import ch.cyberduck.core.exception.AccessDeniedException; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.exception.InteroperabilityException; -import ch.cyberduck.core.exception.LocalAccessDeniedException; import ch.cyberduck.core.exception.LocalNotfoundException; -import ch.cyberduck.core.exception.NotfoundException; -import ch.cyberduck.core.features.AclPermission; import ch.cyberduck.core.features.AttributesFinder; -import ch.cyberduck.core.features.Delete; -import ch.cyberduck.core.features.Encryption; import ch.cyberduck.core.features.Find; -import ch.cyberduck.core.features.Headers; -import ch.cyberduck.core.features.Move; -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; -import ch.cyberduck.core.preferences.PreferencesReader; +import ch.cyberduck.core.transfer.FeatureFilter; import ch.cyberduck.core.transfer.TransferPathFilter; import ch.cyberduck.core.transfer.TransferStatus; import ch.cyberduck.core.transfer.symlink.SymlinkResolver; -import ch.cyberduck.ui.browser.SearchFilterFactory; +import ch.cyberduck.core.transfer.upload.features.DefaultLocalUploadOptionsFilterChain; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.text.MessageFormat; import java.util.EnumSet; +import java.util.Optional; public abstract class AbstractUploadFilter implements TransferPathFilter { private static final Logger log = LogManager.getLogger(AbstractUploadFilter.class); - private final PreferencesReader preferences; - private final Session session; - private final SymlinkResolver symlinkResolver; - private final Filter hidden = SearchFilterFactory.HIDDEN_FILTER; - + private final SymlinkResolver resolver; private final Find find; private final AttributesFinder attribute; - private final UploadFilterOptions options; + private final FeatureFilter chain; + + public AbstractUploadFilter(final SymlinkResolver resolver, final Session session, final UploadFilterOptions options) { + this(resolver, session, session.getFeature(Find.class), session.getFeature(AttributesFinder.class), options); + } - public AbstractUploadFilter(final SymlinkResolver symlinkResolver, final Session session, final UploadFilterOptions options) { - this(symlinkResolver, session, session.getFeature(Find.class), session.getFeature(AttributesFinder.class), options); + public AbstractUploadFilter(final SymlinkResolver resolver, final Session session, final Find find, final AttributesFinder attribute, final UploadFilterOptions options) { + this(resolver, find, attribute, new DefaultLocalUploadOptionsFilterChain(session, options)); } - public AbstractUploadFilter(final SymlinkResolver symlinkResolver, final Session session, final Find find, final AttributesFinder attribute, final UploadFilterOptions options) { - this.session = session; - this.symlinkResolver = symlinkResolver; + public AbstractUploadFilter(final SymlinkResolver resolver, final Find find, final AttributesFinder attribute, final FeatureFilter chain) { + this.resolver = resolver; this.find = find; this.attribute = attribute; - this.options = options; - this.preferences = new HostPreferences(session.getHost()); + this.chain = chain; } @Override @@ -100,35 +73,11 @@ public boolean accept(final Path file, final Local local, final TransferStatus p @Override public TransferStatus prepare(final Path file, final Local local, final TransferStatus parent, final ProgressListener progress) throws BackgroundException { log.debug("Prepare {}", file); - final TransferStatus status = new TransferStatus() - .hidden(!hidden.accept(file)) - .withLockId(parent.getLockId()); - // Read remote attributes first - if(parent.isExists()) { - if(find.find(file)) { - status.setExists(true); - // Read remote attributes - final PathAttributes attributes = attribute.find(file); - status.setRemote(attributes); - } - else { - // Look if there is directory or file that clashes with this upload - if(file.getType().contains(Path.Type.file)) { - if(find.find(new Path(file.getAbsolute(), EnumSet.of(Path.Type.directory)))) { - throw new AccessDeniedException(String.format("Cannot replace folder %s with file %s", file.getAbsolute(), file.getName())); - } - } - if(file.getType().contains(Path.Type.directory)) { - if(find.find(new Path(file.getAbsolute(), EnumSet.of(Path.Type.file)))) { - throw new AccessDeniedException(String.format("Cannot replace file %s with folder %s", file.getAbsolute(), file.getName())); - } - } - } - } + final TransferStatus status = new TransferStatus().withLockId(parent.getLockId()); if(file.isFile()) { // Set content length from local file if(local.isSymbolicLink()) { - if(!symlinkResolver.resolve(local)) { + if(!resolver.resolve(local)) { // Will resolve the symbolic link when the file is requested. final Local target = local.getSymlinkTarget(); status.setLength(target.attributes().getSize()); @@ -139,239 +88,44 @@ public TransferStatus prepare(final Path file, final Local local, final Transfer // Read file size from filesystem status.setLength(local.attributes().getSize()); } - if(options.temporary) { - final Move feature = session.getFeature(Move.class); - final Path renamed = new Path(file.getParent(), - MessageFormat.format(preferences.getProperty("queue.upload.file.temporary.format"), - file.getName(), new AlphanumericRandomStringService().random()), file.getType()); - if(feature.isSupported(file, renamed)) { - log.debug("Set temporary filename {}", renamed); - // Set target name after transfer - status.withRename(renamed).withDisplayname(file); - // Remember status of target file for later rename - status.getDisplayname().exists(status.isExists()); - // Keep exist flag for subclasses to determine additional rename strategy - } - else { - log.warn("Cannot use temporary filename for upload with missing rename support for {}", file); - } - } - status.withMime(new MappingMimeTypeService().getMime(file.getName())); } if(file.isDirectory()) { status.setLength(0L); } - if(options.permissions) { - final UnixPermission feature = session.getFeature(UnixPermission.class); - if(feature != null) { - if(status.isExists()) { - // Already set when reading attributes of file - status.setPermission(status.getRemote().getPermission()); - } - else { - if(new HostPreferences(session.getHost()).getBoolean("queue.upload.permissions.default")) { - status.setPermission(feature.getDefault(file.getType())); - } - else { - // Read permissions from local file - status.setPermission(local.attributes().getPermission()); - } - } - } - else { - // Setting target UNIX permissions in transfer status - status.setPermission(Permission.EMPTY); - } - } - if(options.acl) { - final AclPermission feature = session.getFeature(AclPermission.class); - if(feature != null) { - if(status.isExists()) { - progress.message(MessageFormat.format(LocaleFactory.localizedString("Getting permission of {0}", "Status"), - file.getName())); - try { - status.setAcl(feature.getPermission(file)); - } - catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { - status.setAcl(feature.getDefault(file)); - } - } - else { - status.setAcl(feature.getDefault(file)); - } + // Read remote attributes first + if(parent.isExists()) { + if(find.find(file)) { + status.setExists(true); + // Read remote attributes + status.setRemote(attribute.find(file)); } else { - // Setting target ACL in transfer status - status.setAcl(Acl.EMPTY); - } - } - if(options.timestamp) { - if(1L != local.attributes().getModificationDate()) { - status.setModified(local.attributes().getModificationDate()); - } - if(1L != local.attributes().getCreationDate()) { - status.setCreated(local.attributes().getCreationDate()); - } - } - if(options.metadata) { - final Headers feature = session.getFeature(Headers.class); - if(feature != null) { - if(status.isExists()) { - progress.message(MessageFormat.format(LocaleFactory.localizedString("Reading metadata of {0}", "Status"), - file.getName())); - try { - status.setMetadata(feature.getMetadata(file)); - } - catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { - status.setMetadata(feature.getDefault()); - } - } - else { - status.setMetadata(feature.getDefault()); - } - } - } - if(options.encryption) { - final Encryption feature = session.getFeature(Encryption.class); - if(feature != null) { - if(status.isExists()) { - progress.message(MessageFormat.format(LocaleFactory.localizedString("Reading metadata of {0}", "Status"), - file.getName())); - try { - status.setEncryption(feature.getEncryption(file)); - } - catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { - status.setEncryption(feature.getDefault(file)); - } - } - else { - status.setEncryption(feature.getDefault(file)); - } - } - } - if(options.redundancy) { - if(file.isFile()) { - final Redundancy feature = session.getFeature(Redundancy.class); - if(feature != null) { - if(status.isExists()) { - progress.message(MessageFormat.format(LocaleFactory.localizedString("Reading metadata of {0}", "Status"), - file.getName())); - try { - status.setStorageClass(feature.getClass(file)); - } - catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { - status.setStorageClass(feature.getDefault()); - } - } - else { - status.setStorageClass(feature.getDefault()); + // Look if there is directory or file that clashes with this upload + if(file.getType().contains(Path.Type.file)) { + if(find.find(new Path(file.getAbsolute(), EnumSet.of(Path.Type.directory)))) { + throw new AccessDeniedException(String.format("Cannot replace folder %s with file %s", file.getAbsolute(), file.getName())); } } - } - } - if(options.checksum) { - if(file.isFile()) { - final ChecksumCompute feature = session.getFeature(Write.class).checksum(file, status); - if(feature != null) { - progress.message(MessageFormat.format(LocaleFactory.localizedString("Calculate checksum for {0}", "Status"), - file.getName())); - try { - status.setChecksum(feature.compute(local.getInputStream(), status)); - } - catch(LocalAccessDeniedException e) { - // Ignore failure reading file when in sandbox when we miss a security scoped access bookmark. - // Lock for files is obtained only later in Transfer#pre - log.warn(e.getMessage()); + if(file.getType().contains(Path.Type.directory)) { + if(find.find(new Path(file.getAbsolute(), EnumSet.of(Path.Type.file)))) { + throw new AccessDeniedException(String.format("Cannot replace file %s with folder %s", file.getAbsolute(), file.getName())); } } } } - return status; + return chain.prepare(file, Optional.of(local), status, progress); } @Override - public void apply(final Path file, final Local local, final TransferStatus status, - final ProgressListener listener) throws BackgroundException { - if(file.isFile()) { - if(status.isExists() && !status.isAppend()) { - if(options.versioning) { - switch(session.getHost().getProtocol().getVersioningMode()) { - case custom: - final Versioning feature = session.getFeature(Versioning.class); - if(feature != null && feature.getConfiguration(file).isEnabled()) { - if(feature.save(file)) { - log.debug("Clear exist flag for file {}", file); - status.exists(false).getDisplayname().exists(false); - } - } - } - } - } - } - if(status.getRename().remote != null) { - log.debug("Clear exist flag for file {}", local); - // Reset exist flag after subclass hae applied strategy - status.setExists(false); - } + public void apply(final Path file, final Local local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + chain.apply(file, status, progress); } @Override - public void complete(final Path file, final Local local, - final TransferStatus status, final ProgressListener listener) throws BackgroundException { + public void complete(final Path file, final Local local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { log.debug("Complete {} with status {}", file.getAbsolute(), status); if(status.isComplete()) { - if(!Permission.EMPTY.equals(status.getPermission())) { - final UnixPermission feature = session.getFeature(UnixPermission.class); - if(feature != null) { - try { - listener.message(MessageFormat.format(LocaleFactory.localizedString("Changing permission of {0} to {1}", "Status"), - file.getName(), status.getPermission())); - feature.setUnixPermission(file, status); - } - catch(BackgroundException e) { - // Ignore - log.warn(e.getMessage()); - } - } - } - if(!Acl.EMPTY.equals(status.getAcl())) { - final AclPermission feature = session.getFeature(AclPermission.class); - if(feature != null) { - try { - listener.message(MessageFormat.format(LocaleFactory.localizedString("Changing permission of {0} to {1}", "Status"), - file.getName(), StringUtils.isBlank(status.getAcl().getCannedString()) ? LocaleFactory.localizedString("Unknown") : status.getAcl().getCannedString())); - feature.setPermission(file, status); - } - catch(BackgroundException e) { - // Ignore - log.warn(e.getMessage()); - } - } - } - if(status.getModified() != null) { - if(!session.getFeature(Write.class).timestamp(file)) { - final Timestamp feature = session.getFeature(Timestamp.class); - if(feature != null) { - try { - listener.message(MessageFormat.format(LocaleFactory.localizedString("Changing timestamp of {0} to {1}", "Status"), - file.getName(), UserDateFormatterFactory.get().getShortFormat(status.getModified()))); - feature.setTimestamp(file, status); - } - catch(BackgroundException e) { - // Ignore - log.warn(e.getMessage()); - } - } - } - } - if(file.isFile()) { - if(status.getDisplayname().remote != null) { - final Move move = session.getFeature(Move.class); - log.info("Rename file {} to {}", file, status.getDisplayname().remote); - move.move(file, status.getDisplayname().remote, new TransferStatus(status).exists(status.getDisplayname().exists), - new Delete.DisabledCallback(), new DisabledConnectionCallback()); - } - } + chain.complete(file, Optional.empty(), status, progress); } } -} +} \ No newline at end of file diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/AclFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/AclFeatureFilter.java new file mode 100644 index 00000000000..06c0a0d8750 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/AclFeatureFilter.java @@ -0,0 +1,86 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.Acl; +import ch.cyberduck.core.Local; +import ch.cyberduck.core.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.AccessDeniedException; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.NotfoundException; +import ch.cyberduck.core.features.AclPermission; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Optional; + +public class AclFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(AclFeatureFilter.class); + + private final Session session; + + public AclFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + final AclPermission feature = session.getFeature(AclPermission.class); + if(feature != null) { + if(status.isExists()) { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Getting permission of {0}", "Status"), + file.getName())); + try { + status.setAcl(feature.getPermission(file)); + } + catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { + status.setAcl(feature.getDefault(file)); + } + } + else { + status.setAcl(feature.getDefault(file)); + } + } + return status; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(!Acl.EMPTY.equals(status.getAcl())) { + final AclPermission feature = session.getFeature(AclPermission.class); + if(feature != null) { + try { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Changing permission of {0} to {1}", "Status"), + file.getName(), StringUtils.isBlank(status.getAcl().getCannedString()) ? LocaleFactory.localizedString("Unknown") : status.getAcl().getCannedString())); + feature.setPermission(file, status); + } + catch(BackgroundException e) { + // Ignore + log.warn(e.getMessage()); + } + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/ChecksumFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/ChecksumFeatureFilter.java new file mode 100644 index 00000000000..d526d44d4c0 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/ChecksumFeatureFilter.java @@ -0,0 +1,66 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +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.exception.LocalAccessDeniedException; +import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.io.ChecksumCompute; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Optional; + +public class ChecksumFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(ChecksumFeatureFilter.class); + + private final Session session; + + public ChecksumFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(local.isPresent()) { + if(file.isFile()) { + final ChecksumCompute feature = session.getFeature(Write.class).checksum(file, status); + if(feature != null) { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Calculate checksum for {0}", "Status"), + file.getName())); + try { + status.setChecksum(feature.compute(local.get().getInputStream(), status)); + } + catch(LocalAccessDeniedException e) { + // Ignore failure reading file when in sandbox when we miss a security scoped access bookmark. + // Lock for files is obtained only later in Transfer#pre + log.warn(e.getMessage()); + } + } + } + } + return status; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/DefaultLocalUploadOptionsFilterChain.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/DefaultLocalUploadOptionsFilterChain.java new file mode 100644 index 00000000000..54bf226d6b5 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/DefaultLocalUploadOptionsFilterChain.java @@ -0,0 +1,39 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.Session; +import ch.cyberduck.core.transfer.ChainedFeatureFilter; +import ch.cyberduck.core.transfer.upload.UploadFilterOptions; + +public final class DefaultLocalUploadOptionsFilterChain extends ChainedFeatureFilter { + + public DefaultLocalUploadOptionsFilterChain(final Session session, final UploadFilterOptions options) { + super( + new MimeFeatureFilter(), + new HiddenFeatureFilter(), + options.temporary ? new TemporaryFeatureFilter(session) : noop, + options.permissions ? new PermissionFeatureFilter(session) : noop, + options.acl ? new AclFeatureFilter(session) : noop, + options.timestamp ? new TimestampFeatureFilter(session) : noop, + options.metadata ? new MetadataFeatureFilter(session) : noop, + options.encryption ? new EncryptionFeatureFilter(session) : noop, + options.redundancy ? new RedundancyClassFeatureFilter(session) : noop, + options.checksum ? new ChecksumFeatureFilter(session) : noop, + options.versioning ? new VersioningFeatureFilter(session) : noop + ); + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/EncryptionFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/EncryptionFeatureFilter.java new file mode 100644 index 00000000000..261aa5e6991 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/EncryptionFeatureFilter.java @@ -0,0 +1,62 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.AccessDeniedException; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.NotfoundException; +import ch.cyberduck.core.features.Encryption; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.text.MessageFormat; +import java.util.Optional; + +public class EncryptionFeatureFilter implements FeatureFilter { + + private final Session session; + + public EncryptionFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + final Encryption feature = session.getFeature(Encryption.class); + if(feature != null) { + if(status.isExists()) { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Reading metadata of {0}", "Status"), + file.getName())); + try { + status.setEncryption(feature.getEncryption(file)); + } + catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { + status.setEncryption(feature.getDefault(file)); + } + } + else { + status.setEncryption(feature.getDefault(file)); + } + } + return status; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/HiddenFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/HiddenFeatureFilter.java new file mode 100644 index 00000000000..1e97887a9b7 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/HiddenFeatureFilter.java @@ -0,0 +1,46 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.Filter; +import ch.cyberduck.core.Local; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; +import ch.cyberduck.ui.browser.SearchFilterFactory; + +import java.util.Optional; + +public class HiddenFeatureFilter implements FeatureFilter { + + private final Filter hidden; + + public HiddenFeatureFilter() { + this(SearchFilterFactory.HIDDEN_FILTER); + } + + public HiddenFeatureFilter(final Filter hidden) { + this.hidden = hidden; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + status.setHidden(!hidden.accept(file)); + return status; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/MetadataFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/MetadataFeatureFilter.java new file mode 100644 index 00000000000..affd3f96f26 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/MetadataFeatureFilter.java @@ -0,0 +1,62 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.AccessDeniedException; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.NotfoundException; +import ch.cyberduck.core.features.Headers; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.text.MessageFormat; +import java.util.Optional; + +public class MetadataFeatureFilter implements FeatureFilter { + + private final Session session; + + public MetadataFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + final Headers feature = session.getFeature(Headers.class); + if(feature != null) { + if(status.isExists()) { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Reading metadata of {0}", "Status"), + file.getName())); + try { + status.setMetadata(feature.getMetadata(file)); + } + catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { + status.setMetadata(feature.getDefault()); + } + } + else { + status.setMetadata(feature.getDefault()); + } + } + return status; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/MimeFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/MimeFeatureFilter.java new file mode 100644 index 00000000000..1b78dc1740d --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/MimeFeatureFilter.java @@ -0,0 +1,48 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.MappingMimeTypeService; +import ch.cyberduck.core.MimeTypeService; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.util.Optional; + +public class MimeFeatureFilter implements FeatureFilter { + + private final MimeTypeService service; + + public MimeFeatureFilter() { + this(new MappingMimeTypeService()); + } + + public MimeFeatureFilter(final MimeTypeService service) { + this.service = service; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + status.setMime(service.getMime(file.getName())); + } + return status; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/PermissionFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/PermissionFeatureFilter.java new file mode 100644 index 00000000000..d3302d3c6f0 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/PermissionFeatureFilter.java @@ -0,0 +1,88 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.Permission; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.UnixPermission; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Optional; + +public class PermissionFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(PermissionFeatureFilter.class); + + private final Session session; + + public PermissionFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + final UnixPermission feature = session.getFeature(UnixPermission.class); + if(feature != null) { + if(status.isExists()) { + // Already set when reading attributes of file + status.setPermission(status.getRemote().getPermission()); + } + else { + if(new HostPreferences(session.getHost()).getBoolean("queue.upload.permissions.default")) { + status.setPermission(feature.getDefault(file.getType())); + } + else { + if(local.isPresent()) { + // Read permissions from local file + status.setPermission(local.get().attributes().getPermission()); + } + else { + status.setPermission(feature.getDefault(file.getType())); + } + } + } + } + return status; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(!Permission.EMPTY.equals(status.getPermission())) { + final UnixPermission feature = session.getFeature(UnixPermission.class); + if(feature != null) { + try { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Changing permission of {0} to {1}", "Status"), + file.getName(), status.getPermission())); + feature.setUnixPermission(file, status); + } + catch(BackgroundException e) { + // Ignore + log.warn(e.getMessage()); + } + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/RedundancyClassFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/RedundancyClassFeatureFilter.java new file mode 100644 index 00000000000..fe737de1466 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/RedundancyClassFeatureFilter.java @@ -0,0 +1,64 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.AccessDeniedException; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.NotfoundException; +import ch.cyberduck.core.features.Redundancy; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import java.text.MessageFormat; +import java.util.Optional; + +public class RedundancyClassFeatureFilter implements FeatureFilter { + + private final Session session; + + public RedundancyClassFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + final Redundancy feature = session.getFeature(Redundancy.class); + if(feature != null) { + if(status.isExists()) { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Reading metadata of {0}", "Status"), + file.getName())); + try { + status.setStorageClass(feature.getClass(file)); + } + catch(NotfoundException | AccessDeniedException | InteroperabilityException e) { + status.setStorageClass(feature.getDefault()); + } + } + else { + status.setStorageClass(feature.getDefault()); + } + } + } + return status; + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/TemporaryFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/TemporaryFeatureFilter.java new file mode 100644 index 00000000000..8a3bdf25fe0 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/TemporaryFeatureFilter.java @@ -0,0 +1,88 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.AlphanumericRandomStringService; +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.Move; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Optional; + +public class TemporaryFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(TemporaryFeatureFilter.class); + + private final Session session; + + public TemporaryFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + final Move feature = session.getFeature(Move.class); + final Path renamed = new Path(file.getParent(), + MessageFormat.format(new HostPreferences(session.getHost()).getProperty("queue.upload.file.temporary.format"), + file.getName(), new AlphanumericRandomStringService().random()), file.getType()); + if(feature.isSupported(file, renamed)) { + log.debug("Set temporary filename {}", renamed); + // Set target name after transfer + status.withRename(renamed).withDisplayname(file); + // Remember status of target file for later rename + status.getDisplayname().exists(status.isExists()); + // Keep exist flag for subclasses to determine additional rename strategy + } + else { + log.warn("Cannot use temporary filename for upload with missing rename support for {}", file); + } + } + return status; + } + + @Override + public void apply(final Path file, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(status.getRename().remote != null) { + log.debug("Clear exist flag for file {}", file); + // Reset exist flag after subclass has applied strategy + status.setExists(false); + } + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + if(status.getDisplayname().remote != null) { + log.info("Rename file {} to {}", file, status.getDisplayname().remote); + final Move feature = session.getFeature(Move.class); + feature.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/features/TimestampFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/TimestampFeatureFilter.java new file mode 100644 index 00000000000..99f6ee28158 --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/TimestampFeatureFilter.java @@ -0,0 +1,77 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.LocaleFactory; +import ch.cyberduck.core.Path; +import ch.cyberduck.core.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.UserDateFormatterFactory; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Timestamp; +import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Optional; + +public class TimestampFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(TimestampFeatureFilter.class); + + private final Session session; + + public TimestampFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public TransferStatus prepare(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(local.isPresent()) { + if(1L != local.get().attributes().getModificationDate()) { + status.setModified(local.get().attributes().getModificationDate()); + } + if(1L != local.get().attributes().getCreationDate()) { + status.setCreated(local.get().attributes().getCreationDate()); + } + } + return status; + } + + @Override + public void complete(final Path file, final Optional local, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(status.getModified() != null) { + if(!session.getFeature(Write.class).timestamp(file)) { + final Timestamp feature = session.getFeature(Timestamp.class); + if(feature != null) { + try { + progress.message(MessageFormat.format(LocaleFactory.localizedString("Changing timestamp of {0} to {1}", "Status"), + file.getName(), UserDateFormatterFactory.get().getShortFormat(status.getModified()))); + feature.setTimestamp(file, status); + } + catch(BackgroundException e) { + // Ignore + log.warn(e.getMessage()); + } + } + } + } + } +} diff --git a/core/src/main/java/ch/cyberduck/core/transfer/upload/features/VersioningFeatureFilter.java b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/VersioningFeatureFilter.java new file mode 100644 index 00000000000..52f52ea435c --- /dev/null +++ b/core/src/main/java/ch/cyberduck/core/transfer/upload/features/VersioningFeatureFilter.java @@ -0,0 +1,55 @@ +package ch.cyberduck.core.transfer.upload.features; + +/* + * Copyright (c) 2002-2024 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.ProgressListener; +import ch.cyberduck.core.Session; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Versioning; +import ch.cyberduck.core.transfer.FeatureFilter; +import ch.cyberduck.core.transfer.TransferStatus; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class VersioningFeatureFilter implements FeatureFilter { + private static final Logger log = LogManager.getLogger(VersioningFeatureFilter.class); + + private final Session session; + + public VersioningFeatureFilter(final Session session) { + this.session = session; + } + + @Override + public void apply(final Path file, final TransferStatus status, final ProgressListener progress) throws BackgroundException { + if(file.isFile()) { + if(status.isExists() && !status.isAppend()) { + switch(session.getHost().getProtocol().getVersioningMode()) { + case custom: + final Versioning feature = session.getFeature(Versioning.class); + if(feature != null && feature.getConfiguration(file).isEnabled()) { + if(feature.save(file)) { + log.debug("Clear exist flag for file {}", file); + status.exists(false).getDisplayname().exists(false); + } + } + } + } + } + } +} diff --git a/core/src/test/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilterTest.java b/core/src/test/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilterTest.java index 4913dfba206..d9ee443c9bb 100644 --- a/core/src/test/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilterTest.java +++ b/core/src/test/java/ch/cyberduck/core/transfer/download/AbstractDownloadFilterTest.java @@ -15,6 +15,8 @@ * GNU General Public License for more details. */ +import ch.cyberduck.core.transfer.download.features.SegmentedFeatureFilter; + import org.junit.Test; import static ch.cyberduck.core.transfer.download.AbstractDownloadFilterTest.Unit.GiB; @@ -45,47 +47,47 @@ public void testFindSegmentSize() { final SegmentSizePair[] tests = new SegmentSizePair[]{ // split 20 MiB on one connection down to two 10 MiB segments new SegmentSizePair( - convertSize(20, MiB), 1, - convertSize(10, MiB), convertSize(128, MiB), 128) + convertSize(20, MiB), 1, + convertSize(10, MiB), convertSize(128, MiB), 128) .withExpected(convertSize(10, MiB)), // split 16 GiB down to 128 segments of size 128 MiB new SegmentSizePair( - convertSize(16, GiB), 1, - convertSize(10, MiB), convertSize(128, MiB), 128) + convertSize(16, GiB), 1, + convertSize(10, MiB), convertSize(128, MiB), 128) .withExpected(convertSize(128, MiB)), // halving allowed segments increases segment size for 16 GiB file new SegmentSizePair( - convertSize(16, GiB), 1, - convertSize(10, MiB), convertSize(128, MiB), 64) + convertSize(16, GiB), 1, + convertSize(10, MiB), convertSize(128, MiB), 64) .withExpected(convertSize(256, MiB)), // doubling file size new SegmentSizePair( - convertSize(32, GiB), 1, - convertSize(10, MiB), convertSize(128, MiB), 128) + convertSize(32, GiB), 1, + convertSize(10, MiB), convertSize(128, MiB), 128) .withExpected(convertSize(256, MiB)), new SegmentSizePair( - convertSize(20, MiB) + 1, 1, - convertSize(10, MiB), convertSize(128, MiB), 128) + convertSize(20, MiB) + 1, 1, + convertSize(10, MiB), convertSize(128, MiB), 128) .withExpected(convertSize(5, MiB)), new SegmentSizePair( - convertSize(20, GiB), 1, - convertSize(10, MiB), convertSize(128, MiB), 128) + convertSize(20, GiB), 1, + convertSize(10, MiB), convertSize(128, MiB), 128) .withExpected(convertSize(256, MiB)), new SegmentSizePair( - 4893263872L, 2, - convertSize(10, MiB), convertSize(128, MiB), 128) + 4893263872L, 2, + convertSize(10, MiB), convertSize(128, MiB), 128) .withExpected(38228624L) }; for(final SegmentSizePair test : tests) { assertEquals(test.toString(), test.expected, - AbstractDownloadFilter.findSegmentSize( + SegmentedFeatureFilter.findSegmentSize( test.length, test.connections, test.segmentThreshold, test.segmentSizeMaximum, test.segmentCount)); } } - class SegmentSizePair { + static class SegmentSizePair { public final long length; public final int connections; public final long segmentThreshold;