diff --git a/defaults/src/main/resources/default.properties b/defaults/src/main/resources/default.properties index 6e4248fb230..f99b65beb30 100644 --- a/defaults/src/main/resources/default.properties +++ b/defaults/src/main/resources/default.properties @@ -533,6 +533,8 @@ webdav.microsoftiis.header.translate=true webdav.list.handler.sax=true webdav.lock.enable=true webdav.listing.chunksize=20 +nextcloud.root.default=/remote.php/dav +owncloud.root.default=/remote.php/dav smb.domain.default=WORKGROUP # Enable distributed filesystem path resolver diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeature.java index 06af6fc2d89..81aeda0808b 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeature.java @@ -18,8 +18,8 @@ import ch.cyberduck.core.ListProgressListener; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; +import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.dav.DAVAttributesFinderFeature; -import ch.cyberduck.core.dav.DAVPathEncoder; import ch.cyberduck.core.dav.DAVSession; import ch.cyberduck.core.dav.DAVTimestampFeature; import ch.cyberduck.core.exception.BackgroundException; @@ -92,17 +92,17 @@ protected PathAttributes head(final Path file) { } @Override - protected List list(final Path file) throws IOException { - final String url; + protected List list(final Path file) throws IOException, BackgroundException { + final String path; if(StringUtils.isNotBlank(file.attributes().getVersionId())) { - url = String.format("%sversions/%s/%s", - new DAVPathEncoder().encode(new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions)), + path = String.format("%s/versions/%s/%s", + new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), file.attributes().getFileId(), file.attributes().getVersionId()); } else { - url = new DAVPathEncoder().encode(file); + path = file.getAbsolute(); } - return session.getClient().list(url, 0, + return session.getClient().list(URIEncoder.encode(path), 0, Stream.of(OC_FILEID_CUSTOM_NAMESPACE, OC_CHECKSUMS_CUSTOM_NAMESPACE, OC_SIZE_CUSTOM_NAMESPACE, DAVTimestampFeature.LAST_MODIFIED_CUSTOM_NAMESPACE, DAVTimestampFeature.LAST_MODIFIED_SERVER_CUSTOM_NAMESPACE).collect(Collectors.toSet())); diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeature.java index dcf5dd39637..954d3ea8354 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeature.java @@ -17,7 +17,12 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.Path; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Home; +import ch.cyberduck.core.preferences.HostPreferences; import ch.cyberduck.core.shared.AbstractHomeFeature; +import ch.cyberduck.core.shared.DefaultPathHomeFeature; +import ch.cyberduck.core.shared.DelegatingHomeFeature; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -28,27 +33,51 @@ public class NextcloudHomeFeature extends AbstractHomeFeature { private static final Logger log = LogManager.getLogger(NextcloudHomeFeature.class); + private final Home delegate; private final Host bookmark; + private final String root; public NextcloudHomeFeature(final Host bookmark) { + this(new DefaultPathHomeFeature(bookmark), bookmark); + } + + public NextcloudHomeFeature(final Home delegate, final Host bookmark) { + this(delegate, bookmark, new HostPreferences(bookmark).getProperty("nextcloud.root.default")); + } + + /** + * @param root WebDAV root + */ + public NextcloudHomeFeature(final Home delegate, final Host bookmark, final String root) { + this.delegate = delegate; this.bookmark = bookmark; + this.root = root; } @Override - public Path find() { + public Path find() throws BackgroundException { return this.find(Context.files); } - public Path find(final Context files) { - String username = bookmark.getCredentials().getUsername(); + public Path find(final Context files) throws BackgroundException { + final String username = bookmark.getCredentials().getUsername(); if(StringUtils.isBlank(username)) { if(log.isWarnEnabled()) { log.warn(String.format("Missing username for %s", bookmark)); } - return null; + return delegate.find(); + } + // Custom path setting + final Path workdir; + final Path defaultpath = new DelegatingHomeFeature(delegate).find(); + if(!defaultpath.isRoot() && StringUtils.isNotBlank(StringUtils.removeStart(defaultpath.getAbsolute(), root))) { + workdir = new Path(new Path(String.format("%s/%s/%s", root, files.name(), username), EnumSet.of(Path.Type.directory)), + StringUtils.removeStart(defaultpath.getAbsolute(), root), EnumSet.of(Path.Type.directory)); + } + else { + workdir = new Path(new Path(String.format("%s/%s", root, files.name()), EnumSet.of(Path.Type.directory)), + username, EnumSet.of(Path.Type.directory)); } - final Path workdir = new Path(new Path(String.format("/remote.php/dav/%s", files.name()), EnumSet.of(Path.Type.directory)), - username, EnumSet.of(Path.Type.directory)); if(log.isDebugEnabled()) { log.debug(String.format("Use home directory %s", workdir)); } @@ -57,6 +86,7 @@ public Path find(final Context files) { public enum Context { files, - versions + versions, + meta } } diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudReadFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudReadFeature.java index 3e5094cf502..2c1dc64645e 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudReadFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudReadFeature.java @@ -16,9 +16,10 @@ */ import ch.cyberduck.core.Path; -import ch.cyberduck.core.dav.DAVPathEncoder; +import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.dav.DAVReadFeature; import ch.cyberduck.core.dav.DAVSession; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.transfer.TransferStatus; import org.apache.commons.lang3.StringUtils; @@ -36,12 +37,12 @@ public NextcloudReadFeature(final DAVSession session) { } @Override - protected HttpRequestBase toRequest(final Path file, final TransferStatus status) { + protected HttpRequestBase toRequest(final Path file, final TransferStatus status) throws BackgroundException { final HttpRequestBase request = super.toRequest(file, status); if(StringUtils.isNotBlank(file.attributes().getVersionId())) { - request.setURI(URI.create(String.format("%sversions/%s/%s", - new DAVPathEncoder().encode(new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions)), - file.attributes().getFileId(), file.attributes().getVersionId()))); + request.setURI(URI.create(URIEncoder.encode(String.format("%s/versions/%s/%s", + new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), + file.attributes().getFileId(), file.attributes().getVersionId())))); } return request; } diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudSession.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudSession.java index e7ba2943742..33751a773ea 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudSession.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudSession.java @@ -15,16 +15,22 @@ * GNU General Public License for more details. */ +import ch.cyberduck.core.DefaultIOExceptionMappingService; import ch.cyberduck.core.Host; +import ch.cyberduck.core.HostKeyCallback; import ch.cyberduck.core.ListService; +import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.UrlProvider; +import ch.cyberduck.core.dav.DAVClient; import ch.cyberduck.core.dav.DAVDirectoryFeature; import ch.cyberduck.core.dav.DAVSession; import ch.cyberduck.core.dav.DAVTouchFeature; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.features.AttributesFinder; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Home; +import ch.cyberduck.core.features.Lock; import ch.cyberduck.core.features.Read; import ch.cyberduck.core.features.Share; import ch.cyberduck.core.features.Timestamp; @@ -32,24 +38,57 @@ import ch.cyberduck.core.features.Upload; import ch.cyberduck.core.features.Versioning; import ch.cyberduck.core.features.Write; +import ch.cyberduck.core.http.DefaultHttpResponseExceptionMappingService; import ch.cyberduck.core.http.HttpUploadFeature; -import ch.cyberduck.core.shared.DefaultPathHomeFeature; +import ch.cyberduck.core.ocs.OcsCapabilities; +import ch.cyberduck.core.ocs.OcsCapabilitiesRequest; +import ch.cyberduck.core.ocs.OcsCapabilitiesResponseHandler; +import ch.cyberduck.core.proxy.Proxy; import ch.cyberduck.core.shared.DelegatingHomeFeature; import ch.cyberduck.core.shared.WorkdirHomeFeature; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; +import ch.cyberduck.core.threading.CancelCallback; + +import org.apache.http.client.HttpResponseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; public class NextcloudSession extends DAVSession { + private static final Logger log = LogManager.getLogger(NextcloudSession.class); + + protected final OcsCapabilities ocs = new OcsCapabilities(); public NextcloudSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { super(host, trust, key); } + @Override + protected DAVClient connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { + return super.connect(proxy, key, prompt, cancel); + } + + @Override + public void login(final Proxy proxy, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { + super.login(proxy, prompt, cancel); + try { + client.execute(new OcsCapabilitiesRequest(host), new OcsCapabilitiesResponseHandler(ocs)); + } + catch(HttpResponseException e) { + throw new DefaultHttpResponseExceptionMappingService().map(e); + } + catch(IOException e) { + throw new DefaultIOExceptionMappingService().map(e); + } + } + @Override @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == Home.class) { - return (T) new DelegatingHomeFeature(new WorkdirHomeFeature(host), new DefaultPathHomeFeature(host), new NextcloudHomeFeature(host)); + return (T) new DelegatingHomeFeature(new WorkdirHomeFeature(host), new NextcloudHomeFeature(host)); } if(type == ListService.class) { return (T) new NextcloudListService(this); @@ -63,6 +102,11 @@ public T _getFeature(final Class type) { if(type == AttributesFinder.class) { return (T) new NextcloudAttributesFinderFeature(this); } + if(type == Lock.class) { + if(!ocs.locking) { + return null; + } + } if(type == Upload.class) { return (T) new HttpUploadFeature(new NextcloudWriteFeature(this)); } @@ -76,6 +120,9 @@ public T _getFeature(final Class type) { return (T) new NextcloudShareFeature(this); } if(type == Versioning.class) { + if(!ocs.versioning) { + return null; + } return (T) new NextcloudVersioningFeature(this); } if(type == Delete.class) { diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudShareFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudShareFeature.java index b0185aa8c04..ebd2ca513ca 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudShareFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudShareFeature.java @@ -24,42 +24,36 @@ import ch.cyberduck.core.PasswordCallback; import ch.cyberduck.core.Path; import ch.cyberduck.core.PathRelativizer; -import ch.cyberduck.core.StringAppender; import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.dav.DAVSession; import ch.cyberduck.core.exception.BackgroundException; -import ch.cyberduck.core.features.Share; import ch.cyberduck.core.http.DefaultHttpResponseExceptionMappingService; +import ch.cyberduck.core.ocs.OcsDownloadShareResponseHandler; +import ch.cyberduck.core.ocs.OcsShareeResponseHandler; +import ch.cyberduck.core.ocs.OcsUploadShareResponseHandler; +import ch.cyberduck.core.ocs.model.Share; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; import org.apache.http.client.HttpResponseException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; -import org.apache.http.entity.BufferedHttpEntity; import org.apache.http.entity.ContentType; -import org.apache.http.impl.client.AbstractResponseHandler; -import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.IOException; -import java.net.URI; import java.text.MessageFormat; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.github.sardine.impl.handler.VoidResponseHandler; -public class NextcloudShareFeature implements Share { +public class NextcloudShareFeature implements ch.cyberduck.core.features.Share { private static final Logger log = LogManager.getLogger(NextcloudShareFeature.class); private final DAVSession session; @@ -107,26 +101,7 @@ public Set getSharees(final Type type) throws BackgroundException { Collections.singletonList(Sharee.world) ); try { - sharees.addAll(session.getClient().execute(resource, new OcsResponseHandler>() { - @Override - public Set handleEntity(final HttpEntity entity) throws IOException { - final XmlMapper mapper = new XmlMapper(); - final ocs value = mapper.readValue(entity.getContent(), ocs.class); - if(value.data != null) { - if(value.data.users != null) { - final Set sharees = new HashSet<>(); - for(ocs.user user : value.data.users) { - final String id = user.value.shareWith; - final String label = String.format("%s (%s)", user.label, user.shareWithDisplayNameUnique); - sharees.add(new Sharee(id, label)); - } - return sharees; - } - } - return Collections.emptySet(); - } - } - )); + sharees.addAll(session.getClient().execute(resource, new OcsShareeResponseHandler())); } catch(HttpResponseException e) { log.warn(String.format("Failure %s retrieving sharees", e)); @@ -163,19 +138,7 @@ public DescriptiveUrl toDownloadUrl(final Path file, final Sharee sharee, final resource.setHeader("OCS-APIRequest", "true"); resource.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_XML.getMimeType()); try { - return session.getClient().execute(resource, new OcsResponseHandler() { - @Override - public DescriptiveUrl handleEntity(final HttpEntity entity) throws IOException { - final XmlMapper mapper = new XmlMapper(); - final ocs value = mapper.readValue(entity.getContent(), ocs.class); - if(null != value.data) { - if(null != value.data.url) { - return new DescriptiveUrl(URI.create(value.data.url), DescriptiveUrl.Type.http); - } - } - return DescriptiveUrl.EMPTY; - } - }); + return session.getClient().execute(resource, new OcsDownloadShareResponseHandler()); } catch(HttpResponseException e) { throw new DefaultHttpResponseExceptionMappingService().map(e); @@ -207,27 +170,22 @@ public DescriptiveUrl toUploadUrl(final Path file, final Sharee sharee, final Ob resource.setHeader("OCS-APIRequest", "true"); resource.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_XML.getMimeType()); try { - return session.getClient().execute(resource, new OcsResponseHandler() { + return session.getClient().execute(resource, new OcsUploadShareResponseHandler() { @Override public DescriptiveUrl handleEntity(final HttpEntity entity) throws IOException { final XmlMapper mapper = new XmlMapper(); - final ocs value = mapper.readValue(entity.getContent(), ocs.class); - if(null != value.data) { - // Additional request, because permissions are ignored in POST - final StringBuilder request = new StringBuilder(String.format("https://%s/ocs/v1.php/apps/files_sharing/api/v1/shares/%s?permissions=%d", - bookmark.getHostname(), - value.data.id, - SHARE_PERMISSIONS_CREATE - )); - final HttpPut put = new HttpPut(request.toString()); - put.setHeader("OCS-APIRequest", "true"); - put.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_XML.getMimeType()); - session.getClient().execute(put, new VoidResponseHandler()); - if(null != value.data.url) { - return new DescriptiveUrl(URI.create(value.data.url), DescriptiveUrl.Type.http); - } - } - return DescriptiveUrl.EMPTY; + final Share value = mapper.readValue(entity.getContent(), Share.class); + // Additional request, because permissions are ignored in POST + final StringBuilder request = new StringBuilder(String.format("https://%s/ocs/v1.php/apps/files_sharing/api/v1/shares/%s?permissions=%d", + bookmark.getHostname(), + value.data.id, + SHARE_PERMISSIONS_CREATE + )); + final HttpPut put = new HttpPut(request.toString()); + put.setHeader("OCS-APIRequest", "true"); + put.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_XML.getMimeType()); + session.getClient().execute(put, new VoidResponseHandler()); + return super.handleEntity(entity); } }); } @@ -239,112 +197,4 @@ public DescriptiveUrl handleEntity(final HttpEntity entity) throws IOException { } } - /* - - - ok - 200 - OK - - - 36 - 3 - dkocher - David Kocher - 1 - 1559218292 - - - 79NKo6JxmsxxGBb - dkocher - - - David Kocher - /sandbox/example.png - file - image/png - home::dkocher - 3 - 36285 - 36285 - 36275 - /Monte Panarotta.png - - - - - https://example.net/s/67hgsdfjkds67 - 1 - 0 - - - */ - @JsonIgnoreProperties(ignoreUnknown = true) - private static final class ocs { - public meta meta; - public data data; - - @JsonIgnoreProperties(ignoreUnknown = true) - private static final class meta { - public String status; - public String statuscode; - public String message; - public int itemsperpage; - public int totalitems; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private static final class data { - public String id; - public String url; - public user[] users; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private static final class users { - public user[] element; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private static final class user { - public String label; - public String icon; - public String shareWithDisplayNameUnique; - public value value; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - private static final class value { - public int shareType; - public String shareWith; - public String shareWithAdditionalInfo; - } - } - - private abstract static class OcsResponseHandler extends AbstractResponseHandler { - @Override - public R handleResponse(final HttpResponse response) throws IOException { - final StatusLine statusLine = response.getStatusLine(); - EntityUtils.updateEntity(response, new BufferedHttpEntity(response.getEntity())); - if(statusLine.getStatusCode() >= 300) { - final StringAppender message = new StringAppender(); - message.append(statusLine.getReasonPhrase()); - final ocs error = new XmlMapper().readValue(response.getEntity().getContent(), ocs.class); - message.append(error.meta.message); - throw new HttpResponseException(statusLine.getStatusCode(), message.toString()); - } - final ocs error = new XmlMapper().readValue(response.getEntity().getContent(), ocs.class); - try { - if(Integer.parseInt(error.meta.statuscode) > 100) { - final StringAppender message = new StringAppender(); - message.append(error.meta.message); - throw new HttpResponseException(Integer.parseInt(error.meta.statuscode), message.toString()); - } - } - catch(NumberFormatException e) { - log.warn(String.format("Failure parsing status code in response %s", error)); - } - return super.handleResponse(response); - } - } } diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudTimestampFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudTimestampFeature.java index 08a6c729c8f..b94e8b80dca 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudTimestampFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudTimestampFeature.java @@ -17,6 +17,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.dav.DAVTimestampFeature; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; import java.io.IOException; @@ -34,7 +35,7 @@ public NextcloudTimestampFeature(final NextcloudSession session) { } @Override - protected DavResource getResource(final Path file) throws NotfoundException, IOException { + protected DavResource getResource(final Path file) throws BackgroundException, IOException { final Optional optional = new NextcloudAttributesFinderFeature(session).list(file).stream().findFirst(); if(!optional.isPresent()) { throw new NotfoundException(file.getAbsolute()); diff --git a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java index fee10054ed9..7af7c5b5d03 100644 --- a/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java +++ b/nextcloud/src/main/java/ch/cyberduck/core/nextcloud/NextcloudVersioningFeature.java @@ -21,9 +21,9 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.PathAttributes; import ch.cyberduck.core.PathNormalizer; +import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.VersioningConfiguration; import ch.cyberduck.core.dav.DAVExceptionMappingService; -import ch.cyberduck.core.dav.DAVPathEncoder; import ch.cyberduck.core.dav.DAVSession; import ch.cyberduck.core.dav.DAVTimestampFeature; import ch.cyberduck.core.exception.BackgroundException; @@ -72,10 +72,12 @@ public void setConfiguration(final Path container, final PasswordCallback prompt public void revert(final Path file) throws BackgroundException { // To restore a version all that needs to be done is to move a version the special restore folder at /remote.php/dav/versions/USER/restore try { - session.getClient().move(String.format("%sversions/%s/%s", - new DAVPathEncoder().encode(new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions)), - file.attributes().getFileId(), file.attributes().getVersionId()), - String.format("%srestore/target", new DAVPathEncoder().encode(new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions)))); + session.getClient().move(URIEncoder.encode(String.format("%s/versions/%s/%s", + new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), + file.attributes().getFileId(), file.attributes().getVersionId())), + URIEncoder.encode(String.format("%s/restore/target", + new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute())) + ); } catch(SardineException e) { throw new DAVExceptionMappingService().map("Cannot revert file", e, file); @@ -149,9 +151,8 @@ protected boolean filter(final Path file, final DavResource resource) { return true; } - protected List propfind(final Path file, final Propfind body) throws IOException { - return session.getClient().propfind(String.format("%sversions/%s", - new DAVPathEncoder().encode(new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions)), - file.attributes().getFileId()), 1, body); + protected List propfind(final Path file, final Propfind body) throws IOException, BackgroundException { + return session.getClient().propfind(URIEncoder.encode(String.format("%s/versions/%s", + new NextcloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), file.attributes().getFileId())), 1, body); } } diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilities.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilities.java new file mode 100644 index 00000000000..0146dae276d --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilities.java @@ -0,0 +1,47 @@ +package ch.cyberduck.core.ocs;/* + * 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. + */ + +public final class OcsCapabilities { + public static final OcsCapabilities none = new OcsCapabilities(); + + public String webdav; + public boolean versioning; + public boolean locking; + + public OcsCapabilities withWebdav(final String webdav) { + this.webdav = webdav; + return this; + } + + public OcsCapabilities withVersioning(final boolean versioning) { + this.versioning = versioning; + return this; + } + + public OcsCapabilities withLocking(final boolean locking) { + this.locking = locking; + return this; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("OcsCapabilities{"); + sb.append("webdav='").append(webdav).append('\''); + sb.append(", versioning=").append(versioning); + sb.append(", locking=").append(locking); + sb.append('}'); + return sb.toString(); + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilitiesRequest.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilitiesRequest.java new file mode 100644 index 00000000000..0dadb966d2b --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilitiesRequest.java @@ -0,0 +1,33 @@ +package ch.cyberduck.core.ocs; + +/* + * 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.Host; + +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.ContentType; + +public class OcsCapabilitiesRequest extends HttpGet { + + public OcsCapabilitiesRequest(final Host host) { + super(new StringBuilder(String.format("https://%s/ocs/v1.php/cloud/capabilities", + host.getHostname() + )).toString()); + this.setHeader("OCS-APIRequest", "true"); + this.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_XML.getMimeType()); + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilitiesResponseHandler.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilitiesResponseHandler.java new file mode 100644 index 00000000000..308acea37a4 --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsCapabilitiesResponseHandler.java @@ -0,0 +1,67 @@ +package ch.cyberduck.core.ocs; + +/* + * 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.ocs.model.Capabilities; + +import org.apache.http.HttpEntity; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +public class OcsCapabilitiesResponseHandler extends OcsResponseHandler { + private static final Logger log = LogManager.getLogger(OcsCapabilitiesResponseHandler.class); + + private final OcsCapabilities capabilities; + + public OcsCapabilitiesResponseHandler(final OcsCapabilities capabilities) { + this.capabilities = capabilities; + } + + @Override + public OcsCapabilities handleEntity(final HttpEntity entity) throws IOException { + final XmlMapper mapper = new XmlMapper(); + final Capabilities value = mapper.readValue(entity.getContent(), Capabilities.class); + if(value.data != null) { + if(value.data.capabilities != null) { + if(value.data.capabilities.core != null) { + capabilities.withWebdav(value.data.capabilities.core.webdav); + } + if(value.data.capabilities.files != null) { + try { + capabilities.withLocking(1 == Double.parseDouble(value.data.capabilities.files.locking)); + } + catch(NumberFormatException e) { + log.warn(String.format("Failure parsing %s", value.data.capabilities.files.locking)); + } + try { + capabilities.withVersioning(1 == Integer.parseInt(value.data.capabilities.files.versioning)); + } + catch(NumberFormatException e) { + log.warn(String.format("Failure parsing %s", value.data.capabilities.files.versioning)); + } + } + } + } + if(log.isDebugEnabled()) { + log.debug(String.format("Determined OCS capabilities %s", capabilities)); + } + return capabilities; + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsDownloadShareResponseHandler.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsDownloadShareResponseHandler.java new file mode 100644 index 00000000000..571a059744f --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsDownloadShareResponseHandler.java @@ -0,0 +1,41 @@ +package ch.cyberduck.core.ocs; + +/* + * 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.ocs.model.Share; + +import org.apache.http.HttpEntity; + +import java.io.IOException; +import java.net.URI; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +public class OcsDownloadShareResponseHandler extends OcsResponseHandler { + + @Override + public DescriptiveUrl handleEntity(final HttpEntity entity) throws IOException { + final XmlMapper mapper = new XmlMapper(); + final Share value = mapper.readValue(entity.getContent(), Share.class); + if(null != value.data) { + if(null != value.data.url) { + return new DescriptiveUrl(URI.create(value.data.url), DescriptiveUrl.Type.http); + } + } + return DescriptiveUrl.EMPTY; + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsResponseHandler.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsResponseHandler.java new file mode 100644 index 00000000000..093773dc1fe --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsResponseHandler.java @@ -0,0 +1,62 @@ +package ch.cyberduck.core.ocs; + +/* + * 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.StringAppender; +import ch.cyberduck.core.nextcloud.NextcloudShareFeature; +import ch.cyberduck.core.ocs.model.Share; + +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpResponseException; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.impl.client.AbstractResponseHandler; +import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +public abstract class OcsResponseHandler extends AbstractResponseHandler { + private static final Logger log = LogManager.getLogger(NextcloudShareFeature.class); + + @Override + public R handleResponse(final HttpResponse response) throws IOException { + final StatusLine statusLine = response.getStatusLine(); + EntityUtils.updateEntity(response, new BufferedHttpEntity(response.getEntity())); + if(statusLine.getStatusCode() >= 300) { + final StringAppender message = new StringAppender(); + message.append(statusLine.getReasonPhrase()); + final Share error = new XmlMapper().readValue(response.getEntity().getContent(), Share.class); + message.append(error.meta.message); + throw new HttpResponseException(statusLine.getStatusCode(), message.toString()); + } + final Share error = new XmlMapper().readValue(response.getEntity().getContent(), Share.class); + try { + if(Integer.parseInt(error.meta.statuscode) > 100) { + final StringAppender message = new StringAppender(); + message.append(error.meta.message); + throw new HttpResponseException(Integer.parseInt(error.meta.statuscode), message.toString()); + } + } + catch(NumberFormatException e) { + log.warn(String.format("Failure parsing status code in response %s", error)); + } + return super.handleResponse(response); + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsShareeResponseHandler.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsShareeResponseHandler.java new file mode 100644 index 00000000000..279c0ab3b3a --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsShareeResponseHandler.java @@ -0,0 +1,48 @@ +package ch.cyberduck.core.ocs; + +/* + * 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.features.Share; + +import org.apache.http.HttpEntity; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +public class OcsShareeResponseHandler extends OcsResponseHandler> { + + @Override + public Set handleEntity(final HttpEntity entity) throws IOException { + final XmlMapper mapper = new XmlMapper(); + final ch.cyberduck.core.ocs.model.Share value = mapper.readValue(entity.getContent(), ch.cyberduck.core.ocs.model.Share.class); + if(value.data != null) { + if(value.data.users != null) { + final Set sharees = new HashSet<>(); + for(ch.cyberduck.core.ocs.model.Share.user user : value.data.users) { + final String id = user.value.shareWith; + final String label = String.format("%s (%s)", user.label, user.shareWithDisplayNameUnique); + sharees.add(new Share.Sharee(id, label)); + } + return sharees; + } + } + return Collections.emptySet(); + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsUploadShareResponseHandler.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsUploadShareResponseHandler.java new file mode 100644 index 00000000000..9b8eede4415 --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/OcsUploadShareResponseHandler.java @@ -0,0 +1,41 @@ +package ch.cyberduck.core.ocs; + +/* + * 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.ocs.model.Share; + +import org.apache.http.HttpEntity; + +import java.io.IOException; +import java.net.URI; + +import com.fasterxml.jackson.dataformat.xml.XmlMapper; + +public class OcsUploadShareResponseHandler extends OcsResponseHandler { + + @Override + public DescriptiveUrl handleEntity(final HttpEntity entity) throws IOException { + final XmlMapper mapper = new XmlMapper(); + final Share value = mapper.readValue(entity.getContent(), Share.class); + if(null != value.data) { + if(null != value.data.url) { + return new DescriptiveUrl(URI.create(value.data.url), DescriptiveUrl.Type.http); + } + } + return DescriptiveUrl.EMPTY; + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/model/Capabilities.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/model/Capabilities.java new file mode 100644 index 00000000000..29fd3527d58 --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/model/Capabilities.java @@ -0,0 +1,83 @@ +package ch.cyberduck.core.ocs.model; + +/* + * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/* + + + ok + 100 + OK + + + + + + 17 + 0 + 2 + 17.0.2 + + + + + + 60 + remote.php/webdav + + + +*/ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class Capabilities { + public meta meta; + public data data; + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class meta { + public String status; + public String statuscode; + public String message; + public int itemsperpage; + public int totalitems; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class data { + public capabilities capabilities; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class capabilities { + public core core; + public files files; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class core { + @JsonProperty("webdav-root") + public String webdav; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class files { + public String locking; + public String versioning; + } +} diff --git a/nextcloud/src/main/java/ch/cyberduck/core/ocs/model/Share.java b/nextcloud/src/main/java/ch/cyberduck/core/ocs/model/Share.java new file mode 100644 index 00000000000..aaa7d4ad3e9 --- /dev/null +++ b/nextcloud/src/main/java/ch/cyberduck/core/ocs/model/Share.java @@ -0,0 +1,100 @@ +package ch.cyberduck.core.ocs.model; + +/* + * 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 com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/* + + + ok + 200 + OK + + + 36 + 3 + dkocher + David Kocher + 1 + 1559218292 + + + 79NKo6JxmsxxGBb + dkocher + + + David Kocher + /sandbox/example.png + file + image/png + home::dkocher + 3 + 36285 + 36285 + 36275 + /Monte Panarotta.png + + + + + https://example.net/s/67hgsdfjkds67 + 1 + 0 + + +*/ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class Share { + public meta meta; + public data data; + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class meta { + public String status; + public String statuscode; + public String message; + public int itemsperpage; + public int totalitems; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class data { + public String id; + public String url; + public user[] users; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class users { + public user[] element; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class user { + public String label; + public String icon; + public String shareWithDisplayNameUnique; + public value value; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class value { + public int shareType; + public String shareWith; + public String shareWithAdditionalInfo; + } +} diff --git a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeatureTest.java b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeatureTest.java index 9f2f4063865..d6364ef313f 100644 --- a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeatureTest.java +++ b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudAttributesFinderFeatureTest.java @@ -14,7 +14,7 @@ import ch.cyberduck.core.dav.DAVLockFeature; import ch.cyberduck.core.dav.DAVTimestampFeature; import ch.cyberduck.core.dav.DAVTouchFeature; -import ch.cyberduck.core.exception.InteroperabilityException; +import ch.cyberduck.core.exception.LockedException; import ch.cyberduck.core.exception.NotfoundException; import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.http.HttpResponseOutputStream; @@ -189,7 +189,7 @@ public void testCustomModified_Epoch() { assertEquals(modified.getTime(), attrs.getModificationDate()); } - @Test(expected = InteroperabilityException.class) + @Test public void testFindLock() throws Exception { final Path test = new DAVTouchFeature(new NextcloudWriteFeature(session), new NextcloudAttributesFinderFeature(session)).touch(new Path(new DefaultHomeFinderService(session).find(), new AlphanumericRandomStringService().random(), EnumSet.of(Path.Type.file)), new TransferStatus()); @@ -197,6 +197,7 @@ public void testFindLock() throws Exception { assertNull(f.find(test).getLockId()); final String lockId = new DAVLockFeature(session).lock(test); assertNotNull(f.find(test).getLockId()); + assertThrows(LockedException.class, () -> new DAVDeleteFeature(session).delete(Collections.singletonList(test), new DisabledPasswordCallback(), new Delete.DisabledCallback())); new DAVLockFeature(session).unlock(test, lockId); new DAVDeleteFeature(session).delete(Collections.singletonList(test), new DisabledPasswordCallback(), new Delete.DisabledCallback()); } diff --git a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeatureTest.java b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeatureTest.java index b06bfa4e388..36248f59d13 100644 --- a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeatureTest.java +++ b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudHomeFeatureTest.java @@ -29,11 +29,23 @@ public class NextcloudHomeFeatureTest { @Test - public void testFind() { + public void testFind() throws Exception { final Host bookmark = new Host(new NextcloudProtocol()); final NextcloudHomeFeature feature = new NextcloudHomeFeature(bookmark); assertNull(feature.find()); bookmark.setCredentials(new Credentials("u")); assertEquals(new Path("/remote.php/dav/files/u", EnumSet.of(Path.Type.directory)), feature.find()); + bookmark.setDefaultPath("/remote.php/dav/"); + assertEquals(new Path("/remote.php/dav/files/u", EnumSet.of(Path.Type.directory)), feature.find()); + bookmark.setDefaultPath("/remote.php/dav"); + assertEquals(new Path("/remote.php/dav/files/u", EnumSet.of(Path.Type.directory)), feature.find()); + bookmark.setDefaultPath("/remote.php/dav/d"); + assertEquals(new Path("/remote.php/dav/files/u/d", EnumSet.of(Path.Type.directory)), feature.find()); + bookmark.setDefaultPath("/remote.php/dav/d/"); + assertEquals(new Path("/remote.php/dav/files/u/d", EnumSet.of(Path.Type.directory)), feature.find()); + bookmark.setDefaultPath("/d"); + assertEquals(new Path("/remote.php/dav/files/u/d", EnumSet.of(Path.Type.directory)), feature.find()); + bookmark.setDefaultPath("/d/"); + assertEquals(new Path("/remote.php/dav/files/u/d", EnumSet.of(Path.Type.directory)), feature.find()); } } \ No newline at end of file diff --git a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudSessionTest.java b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudSessionTest.java new file mode 100644 index 00000000000..3667843d87c --- /dev/null +++ b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudSessionTest.java @@ -0,0 +1,35 @@ +package ch.cyberduck.core.nextcloud; + +/* + * 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.test.IntegrationTest; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@Category(IntegrationTest.class) +public class NextcloudSessionTest extends AbstractNextcloudTest { + + @Test + public void testCapabilities() { + assertNotNull(session.ocs.webdav); + assertTrue(session.ocs.versioning); + assertTrue(session.ocs.locking); + } +} \ No newline at end of file diff --git a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudShareFeatureTest.java b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudShareFeatureTest.java index 6c08bf4bd3e..852e1b34e62 100644 --- a/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudShareFeatureTest.java +++ b/nextcloud/src/test/java/ch/cyberduck/core/nextcloud/NextcloudShareFeatureTest.java @@ -44,7 +44,7 @@ public class NextcloudShareFeatureTest extends AbstractNextcloudTest { @Test - public void testIsSupported() { + public void testIsSupported() throws Exception { final Path home = new NextcloudHomeFeature(session.getHost()).find(); assertTrue(new NextcloudShareFeature(session).isSupported(home, Share.Type.download)); assertTrue(new NextcloudShareFeature(session).isSupported(home, Share.Type.upload)); diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudAttributesFinderFeature.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudAttributesFinderFeature.java index 1cf68a4d0ae..da0e72d910b 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudAttributesFinderFeature.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudAttributesFinderFeature.java @@ -16,9 +16,10 @@ */ import ch.cyberduck.core.Path; -import ch.cyberduck.core.dav.DAVPathEncoder; +import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.dav.DAVSession; import ch.cyberduck.core.dav.DAVTimestampFeature; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.nextcloud.NextcloudAttributesFinderFeature; import ch.cyberduck.core.nextcloud.NextcloudHomeFeature; @@ -41,17 +42,17 @@ public OwncloudAttributesFinderFeature(DAVSession session) { } @Override - protected List list(final Path file) throws IOException { - final String url; + protected List list(final Path file) throws IOException, BackgroundException { + final String path; if(StringUtils.isNotBlank(file.attributes().getVersionId())) { - url = String.format("%s/%s/v/%s", + path = String.format("%s/%s/v/%s", new OwncloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), file.attributes().getFileId(), file.attributes().getVersionId()); } else { - url = new DAVPathEncoder().encode(file); + path = file.getAbsolute(); } - return session.getClient().list(url, 0, + return session.getClient().list(URIEncoder.encode(path), 0, Stream.of(OC_FILEID_CUSTOM_NAMESPACE, OC_CHECKSUMS_CUSTOM_NAMESPACE, OC_SIZE_CUSTOM_NAMESPACE, DAVTimestampFeature.LAST_MODIFIED_CUSTOM_NAMESPACE, DAVTimestampFeature.LAST_MODIFIED_SERVER_CUSTOM_NAMESPACE).collect(Collectors.toSet())); diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudHomeFeature.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudHomeFeature.java index efe7acf1111..1e60822e811 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudHomeFeature.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudHomeFeature.java @@ -17,28 +17,34 @@ import ch.cyberduck.core.Host; import ch.cyberduck.core.Path; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.features.Home; import ch.cyberduck.core.nextcloud.NextcloudHomeFeature; +import ch.cyberduck.core.preferences.HostPreferences; +import ch.cyberduck.core.shared.DefaultPathHomeFeature; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import java.util.EnumSet; - public class OwncloudHomeFeature extends NextcloudHomeFeature { private static final Logger log = LogManager.getLogger(OwncloudHomeFeature.class); public OwncloudHomeFeature(final Host bookmark) { - super(bookmark); + this(new DefaultPathHomeFeature(bookmark), bookmark); + } + + public OwncloudHomeFeature(final Home delegate, final Host bookmark) { + this(delegate, bookmark, new HostPreferences(bookmark).getProperty("owncloud.root.default")); + } + + public OwncloudHomeFeature(final Home delegate, final Host bookmark, final String root) { + super(delegate, bookmark, root); } - public Path find(final Context context) { + public Path find(final Context context) throws BackgroundException { switch(context) { case versions: - final Path workdir = new Path("/remote.php/dav/meta", EnumSet.of(Path.Type.directory)); - if(log.isDebugEnabled()) { - log.debug(String.format("Use home directory %s", workdir)); - } - return workdir; + return super.find(Context.meta); } return super.find(context); } diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudReadFeature.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudReadFeature.java index 97b8af5f42f..f7c9a02a013 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudReadFeature.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudReadFeature.java @@ -16,8 +16,10 @@ */ import ch.cyberduck.core.Path; +import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.dav.DAVReadFeature; import ch.cyberduck.core.dav.DAVSession; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.nextcloud.NextcloudHomeFeature; import ch.cyberduck.core.transfer.TransferStatus; @@ -36,12 +38,12 @@ public OwncloudReadFeature(final DAVSession session) { } @Override - protected HttpRequestBase toRequest(final Path file, final TransferStatus status) { + protected HttpRequestBase toRequest(final Path file, final TransferStatus status) throws BackgroundException { final HttpRequestBase request = super.toRequest(file, status); if(StringUtils.isNotBlank(file.attributes().getVersionId())) { - request.setURI(URI.create(String.format("%s/%s/v/%s", + request.setURI(URI.create(URIEncoder.encode(String.format("%s/%s/v/%s", new OwncloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), - file.attributes().getFileId(), file.attributes().getVersionId()))); + file.attributes().getFileId(), file.attributes().getVersionId())))); } return request; } diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudSession.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudSession.java index d8733be8686..621fe1133e7 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudSession.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudSession.java @@ -22,7 +22,6 @@ import ch.cyberduck.core.ListService; import ch.cyberduck.core.LoginCallback; import ch.cyberduck.core.OAuthTokens; -import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.UrlProvider; import ch.cyberduck.core.dav.DAVClient; import ch.cyberduck.core.dav.DAVDirectoryFeature; @@ -34,6 +33,7 @@ import ch.cyberduck.core.features.Delete; import ch.cyberduck.core.features.Directory; import ch.cyberduck.core.features.Home; +import ch.cyberduck.core.features.Lock; import ch.cyberduck.core.features.Read; import ch.cyberduck.core.features.Share; import ch.cyberduck.core.features.Timestamp; @@ -53,21 +53,23 @@ import ch.cyberduck.core.oauth.OAuth2AuthorizationService; import ch.cyberduck.core.oauth.OAuth2ErrorResponseInterceptor; import ch.cyberduck.core.oauth.OAuth2RequestInterceptor; +import ch.cyberduck.core.ocs.OcsCapabilities; +import ch.cyberduck.core.ocs.OcsCapabilitiesRequest; +import ch.cyberduck.core.ocs.OcsCapabilitiesResponseHandler; import ch.cyberduck.core.proxy.Proxy; -import ch.cyberduck.core.shared.DefaultPathHomeFeature; import ch.cyberduck.core.shared.DelegatingHomeFeature; import ch.cyberduck.core.shared.WorkdirHomeFeature; import ch.cyberduck.core.ssl.X509KeyManager; import ch.cyberduck.core.ssl.X509TrustManager; import ch.cyberduck.core.threading.CancelCallback; import ch.cyberduck.core.tus.TusCapabilities; +import ch.cyberduck.core.tus.TusCapabilitiesRequest; import ch.cyberduck.core.tus.TusCapabilitiesResponseHandler; import ch.cyberduck.core.tus.TusWriteFeature; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.client.HttpResponseException; -import org.apache.http.client.methods.HttpOptions; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -84,7 +86,8 @@ public class OwncloudSession extends DAVSession { private OAuth2RequestInterceptor authorizationService; - private final TusCapabilities capabilities = new TusCapabilities(); + protected final TusCapabilities tus = new TusCapabilities(); + protected final OcsCapabilities ocs = new OcsCapabilities(); public OwncloudSession(final Host host, final X509TrustManager trust, final X509KeyManager key) { super(host, trust, key); @@ -93,19 +96,8 @@ public OwncloudSession(final Host host, final X509TrustManager trust, final X509 @Override protected DAVClient connect(final Proxy proxy, final HostKeyCallback key, final LoginCallback prompt, final CancelCallback cancel) throws BackgroundException { final DAVClient client = super.connect(proxy, key, prompt, cancel); - final TusCapabilities capabilities = this.options(client); - return client; - } - - private TusCapabilities options(final DAVClient client) throws BackgroundException { - final HttpOptions options = new HttpOptions(URIEncoder.encode( - new DelegatingHomeFeature(new DefaultPathHomeFeature(host)).find().getAbsolute())); try { - client.execute(options, new TusCapabilitiesResponseHandler(capabilities)); - if(log.isDebugEnabled()) { - log.debug(String.format("Determined capabilities %s", capabilities)); - } - return capabilities; + client.execute(new TusCapabilitiesRequest(host), new TusCapabilitiesResponseHandler(tus)); } catch(HttpResponseException e) { throw new DefaultHttpResponseExceptionMappingService().map(e); @@ -113,6 +105,7 @@ private TusCapabilities options(final DAVClient client) throws BackgroundExcepti catch(IOException e) { throw new DefaultIOExceptionMappingService().map(e); } + return client; } @Override @@ -147,13 +140,22 @@ public void login(final Proxy proxy, final LoginCallback prompt, final CancelCal } } super.login(proxy, prompt, cancel); + try { + client.execute(new OcsCapabilitiesRequest(host), new OcsCapabilitiesResponseHandler(ocs)); + } + catch(HttpResponseException e) { + throw new DefaultHttpResponseExceptionMappingService().map(e); + } + catch(IOException e) { + throw new DefaultIOExceptionMappingService().map(e); + } } @Override @SuppressWarnings("unchecked") public T _getFeature(final Class type) { if(type == Home.class) { - return (T) new DelegatingHomeFeature(new WorkdirHomeFeature(host), new DefaultPathHomeFeature(host), new OwncloudHomeFeature(host)); + return (T) new DelegatingHomeFeature(new WorkdirHomeFeature(host), new OwncloudHomeFeature(host)); } if(type == ListService.class) { return (T) new NextcloudListService(this); @@ -167,9 +169,14 @@ public T _getFeature(final Class type) { if(type == AttributesFinder.class) { return (T) new OwncloudAttributesFinderFeature(this); } + if(type == Lock.class) { + if(!ocs.locking) { + return null; + } + } if(type == Upload.class) { - if(ArrayUtils.contains(capabilities.versions, TUS_VERSION) && capabilities.extensions.contains(TusCapabilities.Extension.creation)) { - return (T) new OcisUploadFeature(host, client.getClient(), new TusWriteFeature(capabilities, client.getClient()), capabilities); + if(ArrayUtils.contains(tus.versions, TUS_VERSION) && tus.extensions.contains(TusCapabilities.Extension.creation)) { + return (T) new OcisUploadFeature(host, client.getClient(), new TusWriteFeature(tus, client.getClient()), tus); } else { return (T) new HttpUploadFeature(new NextcloudWriteFeature(this)); @@ -185,6 +192,9 @@ public T _getFeature(final Class type) { return (T) new NextcloudShareFeature(this); } if(type == Versioning.class) { + if(!ocs.versioning) { + return null; + } return (T) new OwncloudVersioningFeature(this); } if(type == Delete.class) { diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudTimestampFeature.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudTimestampFeature.java index e7415ae1b33..3dfc2020be0 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudTimestampFeature.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudTimestampFeature.java @@ -17,6 +17,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.dav.DAVTimestampFeature; +import ch.cyberduck.core.exception.BackgroundException; import ch.cyberduck.core.exception.NotfoundException; import java.io.IOException; @@ -34,7 +35,7 @@ public OwncloudTimestampFeature(final OwncloudSession session) { } @Override - protected DavResource getResource(final Path file) throws NotfoundException, IOException { + protected DavResource getResource(final Path file) throws BackgroundException, IOException { final Optional optional = new OwncloudAttributesFinderFeature(session).list(file).stream().findFirst(); if(!optional.isPresent()) { throw new NotfoundException(file.getAbsolute()); diff --git a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudVersioningFeature.java b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudVersioningFeature.java index 9b0f58a45f5..4a51f818791 100644 --- a/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudVersioningFeature.java +++ b/owncloud/src/main/java/ch/cyberduck/core/owncloud/OwncloudVersioningFeature.java @@ -17,6 +17,7 @@ import ch.cyberduck.core.Path; import ch.cyberduck.core.PathNormalizer; +import ch.cyberduck.core.URIEncoder; import ch.cyberduck.core.dav.DAVExceptionMappingService; import ch.cyberduck.core.dav.DAVPathEncoder; import ch.cyberduck.core.dav.DAVSession; @@ -68,9 +69,9 @@ protected boolean filter(final Path file, final DavResource resource) { } @Override - protected List propfind(final Path file, final Propfind body) throws IOException { - return session.getClient().propfind(String.format("%s/%s/v", + protected List propfind(final Path file, final Propfind body) throws IOException, BackgroundException { + return session.getClient().propfind(URIEncoder.encode(String.format("%s/%s/v", new OwncloudHomeFeature(session.getHost()).find(NextcloudHomeFeature.Context.versions).getAbsolute(), - file.attributes().getFileId()), 1, body); + file.attributes().getFileId())), 1, body); } } diff --git a/owncloud/src/test/java/ch/cyberduck/core/owncloud/OwncloudSessionTest.java b/owncloud/src/test/java/ch/cyberduck/core/owncloud/OwncloudSessionTest.java new file mode 100644 index 00000000000..9f55b59d6bd --- /dev/null +++ b/owncloud/src/test/java/ch/cyberduck/core/owncloud/OwncloudSessionTest.java @@ -0,0 +1,34 @@ +package ch.cyberduck.core.owncloud; + +/* + * 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.test.IntegrationTest; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import static org.junit.Assert.*; + +@Category(IntegrationTest.class) +public class OwncloudSessionTest extends AbstractOwncloudTest { + + @Test + public void testCapabilities() { + assertNotNull(session.ocs.webdav); + assertTrue(session.ocs.versioning); + assertFalse(session.ocs.locking); + } +} \ No newline at end of file diff --git a/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesRequest.java b/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesRequest.java new file mode 100644 index 00000000000..f30700ed210 --- /dev/null +++ b/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesRequest.java @@ -0,0 +1,31 @@ +package ch.cyberduck.core.tus; + +/* + * 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.Host; +import ch.cyberduck.core.URIEncoder; +import ch.cyberduck.core.exception.BackgroundException; +import ch.cyberduck.core.shared.DefaultPathHomeFeature; +import ch.cyberduck.core.shared.DelegatingHomeFeature; + +import org.apache.http.client.methods.HttpOptions; + +public class TusCapabilitiesRequest extends HttpOptions { + public TusCapabilitiesRequest(final Host host) throws BackgroundException { + super(URIEncoder.encode( + new DelegatingHomeFeature(new DefaultPathHomeFeature(host)).find().getAbsolute())); + } +} diff --git a/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesResponseHandler.java b/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesResponseHandler.java index cce871cbf70..7f613c6bfbf 100644 --- a/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesResponseHandler.java +++ b/tus/src/main/java/ch/cyberduck/core/tus/TusCapabilitiesResponseHandler.java @@ -75,6 +75,9 @@ public TusCapabilities handleResponse(final HttpResponse response) { } } } + if(log.isDebugEnabled()) { + log.debug(String.format("Determined capabilities %s", capabilities)); + } return capabilities; } } diff --git a/webdav/src/main/java/ch/cyberduck/core/dav/DAVAttributesFinderFeature.java b/webdav/src/main/java/ch/cyberduck/core/dav/DAVAttributesFinderFeature.java index cab9e666ae1..6f90a598f2e 100644 --- a/webdav/src/main/java/ch/cyberduck/core/dav/DAVAttributesFinderFeature.java +++ b/webdav/src/main/java/ch/cyberduck/core/dav/DAVAttributesFinderFeature.java @@ -134,7 +134,7 @@ protected PathAttributes head(final Path file) throws IOException { return attributes; } - protected List list(final Path file) throws IOException { + protected List list(final Path file) throws IOException, BackgroundException { return session.getClient().list(new DAVPathEncoder().encode(file), 0, Stream.of( DAVTimestampFeature.LAST_MODIFIED_CUSTOM_NAMESPACE, diff --git a/webdav/src/main/java/ch/cyberduck/core/dav/DAVReadFeature.java b/webdav/src/main/java/ch/cyberduck/core/dav/DAVReadFeature.java index ea248a944db..7cb895203a4 100644 --- a/webdav/src/main/java/ch/cyberduck/core/dav/DAVReadFeature.java +++ b/webdav/src/main/java/ch/cyberduck/core/dav/DAVReadFeature.java @@ -114,7 +114,7 @@ public InputStream read(final Path file, final TransferStatus status, final Conn } } - protected HttpRequestBase toRequest(final Path file, final TransferStatus status) { + protected HttpRequestBase toRequest(final Path file, final TransferStatus status) throws BackgroundException { final StringBuilder resource = new StringBuilder(new DAVPathEncoder().encode(file)); if(!status.getParameters().isEmpty()) { resource.append("?"); diff --git a/webdav/src/main/java/ch/cyberduck/core/dav/DAVSession.java b/webdav/src/main/java/ch/cyberduck/core/dav/DAVSession.java index e65c3054920..c8f7986e373 100644 --- a/webdav/src/main/java/ch/cyberduck/core/dav/DAVSession.java +++ b/webdav/src/main/java/ch/cyberduck/core/dav/DAVSession.java @@ -150,19 +150,11 @@ public void login(final Proxy proxy, final LoginCallback prompt, final CancelCal new UsernamePasswordCredentials(host.getCredentials().getUsername(), host.getCredentials().getPassword())); client.setCredentials(provider); if(preferences.getBoolean("webdav.basic.preemptive")) { - switch(proxy.getType()) { - case DIRECT: - case SOCKS: - // Enable preemptive authentication. See HttpState#setAuthenticationPreemptive - client.enablePreemptiveAuthentication(host.getHostname(), - host.getPort(), - host.getPort(), - Charset.forName(preferences.getProperty("http.credentials.charset")) - ); - break; - default: - client.disablePreemptiveAuthentication(); - } + client.enablePreemptiveAuthentication(host.getHostname(), + host.getPort(), + host.getPort(), + Charset.forName(preferences.getProperty("http.credentials.charset")) + ); } else { client.disablePreemptiveAuthentication(); diff --git a/webdav/src/main/java/ch/cyberduck/core/dav/DAVTimestampFeature.java b/webdav/src/main/java/ch/cyberduck/core/dav/DAVTimestampFeature.java index a7f40de9a0d..3f474b01c4f 100644 --- a/webdav/src/main/java/ch/cyberduck/core/dav/DAVTimestampFeature.java +++ b/webdav/src/main/java/ch/cyberduck/core/dav/DAVTimestampFeature.java @@ -90,7 +90,7 @@ public void setTimestamp(final Path file, final TransferStatus status) throws Ba * @param file File * @return Latest properties */ - protected DavResource getResource(final Path file) throws NotfoundException, IOException { + protected DavResource getResource(final Path file) throws BackgroundException, IOException { final Optional optional = new DAVAttributesFinderFeature(session).list(file).stream().findFirst(); if(!optional.isPresent()) { throw new NotfoundException(file.getAbsolute());