mirror of
https://github.com/owncloud/android-library.git
synced 2025-06-07 16:06:08 +00:00
Merge pull request #521 from owncloud/master
Merge master into stable for 3.0 app version
This commit is contained in:
commit
a788dc098b
@ -1,6 +1,7 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
|
apply plugin: 'kotlin-parcelize'
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.squareup.okhttp3:okhttp:4.6.0'
|
api 'com.squareup.okhttp3:okhttp:4.6.0'
|
||||||
@ -16,7 +17,7 @@ dependencies {
|
|||||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
|
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshiVersion"
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
testImplementation 'org.robolectric:robolectric:4.8.1'
|
testImplementation 'org.robolectric:robolectric:4.9'
|
||||||
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
|
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,6 @@ import com.owncloud.android.lib.common.http.HttpClient;
|
|||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
import com.owncloud.android.lib.common.http.HttpConstants;
|
||||||
import com.owncloud.android.lib.common.http.methods.HttpBaseMethod;
|
import com.owncloud.android.lib.common.http.methods.HttpBaseMethod;
|
||||||
import com.owncloud.android.lib.common.utils.RandomUtils;
|
import com.owncloud.android.lib.common.utils.RandomUtils;
|
||||||
import com.owncloud.android.lib.resources.status.OwnCloudVersion;
|
|
||||||
import okhttp3.Cookie;
|
import okhttp3.Cookie;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
@ -59,7 +58,6 @@ public class OwnCloudClient extends HttpClient {
|
|||||||
private OwnCloudCredentials mCredentials = null;
|
private OwnCloudCredentials mCredentials = null;
|
||||||
private int mInstanceNumber;
|
private int mInstanceNumber;
|
||||||
private Uri mBaseUri;
|
private Uri mBaseUri;
|
||||||
private OwnCloudVersion mVersion = null;
|
|
||||||
private OwnCloudAccount mAccount;
|
private OwnCloudAccount mAccount;
|
||||||
private final ConnectionValidator mConnectionValidator;
|
private final ConnectionValidator mConnectionValidator;
|
||||||
private Object mRequestMutex = new Object();
|
private Object mRequestMutex = new Object();
|
||||||
@ -185,7 +183,7 @@ public class OwnCloudClient extends HttpClient {
|
|||||||
return (mCredentials instanceof OwnCloudAnonymousCredentials || mAccount == null)
|
return (mCredentials instanceof OwnCloudAnonymousCredentials || mAccount == null)
|
||||||
? Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0)
|
? Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0)
|
||||||
: Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0 + AccountUtils.getUserId(
|
: Uri.parse(mBaseUri + WEBDAV_FILES_PATH_4_0 + AccountUtils.getUserId(
|
||||||
mAccount.getSavedAccount(), getContext()
|
mAccount.getSavedAccount(), getContext()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -194,7 +192,7 @@ public class OwnCloudClient extends HttpClient {
|
|||||||
return mCredentials instanceof OwnCloudAnonymousCredentials
|
return mCredentials instanceof OwnCloudAnonymousCredentials
|
||||||
? Uri.parse(mBaseUri + WEBDAV_UPLOADS_PATH_4_0)
|
? Uri.parse(mBaseUri + WEBDAV_UPLOADS_PATH_4_0)
|
||||||
: Uri.parse(mBaseUri + WEBDAV_UPLOADS_PATH_4_0 + AccountUtils.getUserId(
|
: Uri.parse(mBaseUri + WEBDAV_UPLOADS_PATH_4_0 + AccountUtils.getUserId(
|
||||||
mAccount.getSavedAccount(), getContext()
|
mAccount.getSavedAccount(), getContext()
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -241,14 +239,6 @@ public class OwnCloudClient extends HttpClient {
|
|||||||
HttpUrl.parse(mBaseUri.toString()));
|
HttpUrl.parse(mBaseUri.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public OwnCloudVersion getOwnCloudVersion() {
|
|
||||||
return mVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOwnCloudVersion(OwnCloudVersion version) {
|
|
||||||
mVersion = version;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OwnCloudAccount getAccount() {
|
public OwnCloudAccount getAccount() {
|
||||||
return mAccount;
|
return mAccount;
|
||||||
}
|
}
|
||||||
@ -260,4 +250,4 @@ public class OwnCloudClient extends HttpClient {
|
|||||||
public void setFollowRedirects(boolean followRedirects) {
|
public void setFollowRedirects(boolean followRedirects) {
|
||||||
this.mFollowRedirects = followRedirects;
|
this.mFollowRedirects = followRedirects;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,26 +94,6 @@ public class AccountUtils {
|
|||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the stored server version corresponding to an OC account.
|
|
||||||
*
|
|
||||||
* @param account An OC account
|
|
||||||
* @param context Application context
|
|
||||||
* @return Version of the OC server, according to last check
|
|
||||||
*/
|
|
||||||
public static OwnCloudVersion getServerVersionForAccount(Account account, Context context) {
|
|
||||||
AccountManager ama = AccountManager.get(context);
|
|
||||||
OwnCloudVersion version = null;
|
|
||||||
try {
|
|
||||||
String versionString = ama.getUserData(account, Constants.KEY_OC_VERSION);
|
|
||||||
version = new OwnCloudVersion(versionString);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e, "Couldn't get a the server version for an account");
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return
|
* @return
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
@ -209,11 +189,6 @@ public class AccountUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Constants {
|
public static class Constants {
|
||||||
/**
|
|
||||||
* Version should be 3 numbers separated by dot so it can be parsed by
|
|
||||||
* {@link OwnCloudVersion}
|
|
||||||
*/
|
|
||||||
public static final String KEY_OC_VERSION = "oc_version";
|
|
||||||
/**
|
/**
|
||||||
* Base url should point to owncloud installation without trailing / ie:
|
* Base url should point to owncloud installation without trailing / ie:
|
||||||
* http://server/path or https://owncloud.server
|
* http://server/path or https://owncloud.server
|
||||||
|
@ -184,6 +184,7 @@ public class HttpConstants {
|
|||||||
public static final int HTTP_LOCKED = 423;
|
public static final int HTTP_LOCKED = 423;
|
||||||
// 424 Failed Dependency (WebDAV - RFC 2518)
|
// 424 Failed Dependency (WebDAV - RFC 2518)
|
||||||
public static final int HTTP_FAILED_DEPENDENCY = 424;
|
public static final int HTTP_FAILED_DEPENDENCY = 424;
|
||||||
|
public static final int HTTP_TOO_EARLY = 425;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 5xx Client Error
|
* 5xx Client Error
|
||||||
|
@ -36,7 +36,7 @@ import java.net.URL
|
|||||||
class CopyMethod(
|
class CopyMethod(
|
||||||
val url: URL,
|
val url: URL,
|
||||||
private val destinationUrl: String,
|
private val destinationUrl: String,
|
||||||
private val forceOverride: Boolean
|
private val forceOverride: Boolean = false
|
||||||
) : DavMethod(url) {
|
) : DavMethod(url) {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
public override fun onDavExecute(davResource: DavOCResource): Int {
|
public override fun onDavExecute(davResource: DavOCResource): Int {
|
||||||
|
@ -36,7 +36,7 @@ import java.net.URL
|
|||||||
class MoveMethod(
|
class MoveMethod(
|
||||||
url: URL,
|
url: URL,
|
||||||
private val destinationUrl: String,
|
private val destinationUrl: String,
|
||||||
private val forceOverride: Boolean
|
private val forceOverride: Boolean = false
|
||||||
) : DavMethod(url) {
|
) : DavMethod(url) {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun onDavExecute(davResource: DavOCResource): Int {
|
override fun onDavExecute(davResource: DavOCResource): Int {
|
||||||
|
@ -53,7 +53,7 @@ class PropfindMethod(
|
|||||||
depth = depth,
|
depth = depth,
|
||||||
reqProp = propertiesToRequest,
|
reqProp = propertiesToRequest,
|
||||||
listOfHeaders = super.getRequestHeadersAsHashMap(),
|
listOfHeaders = super.getRequestHeadersAsHashMap(),
|
||||||
callback = { response: Response, hrefRelation: HrefRelation? ->
|
callback = { response: Response, hrefRelation: HrefRelation ->
|
||||||
when (hrefRelation) {
|
when (hrefRelation) {
|
||||||
HrefRelation.MEMBER -> members.add(response)
|
HrefRelation.MEMBER -> members.add(response)
|
||||||
HrefRelation.SELF -> this.root = response
|
HrefRelation.SELF -> this.root = response
|
||||||
|
@ -1,114 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.common.network;
|
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okio.BufferedSink;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Request body that represents a file chunk and include information about the progress when uploading it
|
|
||||||
*
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
public class ChunkFromFileRequestBody extends FileRequestBody {
|
|
||||||
|
|
||||||
private final FileChannel mChannel;
|
|
||||||
private final long mChunkSize;
|
|
||||||
private long mOffset;
|
|
||||||
private long mTransferred;
|
|
||||||
private ByteBuffer mBuffer = ByteBuffer.allocate(4096);
|
|
||||||
|
|
||||||
public ChunkFromFileRequestBody(File file, MediaType contentType, FileChannel channel, long chunkSize) {
|
|
||||||
super(file, contentType);
|
|
||||||
if (channel == null) {
|
|
||||||
throw new IllegalArgumentException("File may not be null");
|
|
||||||
}
|
|
||||||
if (chunkSize <= 0) {
|
|
||||||
throw new IllegalArgumentException("Chunk size must be greater than zero");
|
|
||||||
}
|
|
||||||
this.mChannel = channel;
|
|
||||||
this.mChunkSize = chunkSize;
|
|
||||||
mOffset = 0;
|
|
||||||
mTransferred = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long contentLength() {
|
|
||||||
try {
|
|
||||||
return Math.min(mChunkSize, mChannel.size() - mChannel.position());
|
|
||||||
} catch (IOException e) {
|
|
||||||
return mChunkSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeTo(BufferedSink sink) {
|
|
||||||
int readCount;
|
|
||||||
Iterator<OnDatatransferProgressListener> it;
|
|
||||||
|
|
||||||
try {
|
|
||||||
mChannel.position(mOffset);
|
|
||||||
long size = mFile.length();
|
|
||||||
if (size == 0) {
|
|
||||||
size = -1;
|
|
||||||
}
|
|
||||||
long maxCount = Math.min(mOffset + mChunkSize, mChannel.size());
|
|
||||||
while (mChannel.position() < maxCount) {
|
|
||||||
|
|
||||||
readCount = mChannel.read(mBuffer);
|
|
||||||
|
|
||||||
int bytesToWriteInBuffer = (int) Math.min(readCount, mFile.length() - mTransferred);
|
|
||||||
sink.getBuffer().write(mBuffer.array(), 0, bytesToWriteInBuffer);
|
|
||||||
|
|
||||||
sink.flush();
|
|
||||||
|
|
||||||
mBuffer.clear();
|
|
||||||
if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks
|
|
||||||
mTransferred += readCount;
|
|
||||||
}
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
it = mDataTransferListeners.iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
it.next().onTransferProgress(readCount, mTransferred, size, mFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception exception) {
|
|
||||||
Timber.e(exception, "Transferred " + mTransferred + " bytes from a total of " + mFile.length());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOffset(long offset) {
|
|
||||||
this.mOffset = offset;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,92 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.common.network
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.resources.files.chunks.ChunkedUploadFromFileSystemOperation.Companion.CHUNK_SIZE
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okio.BufferedSink
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Request body that represents a file chunk and include information about the progress when uploading it
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
*/
|
||||||
|
class ChunkFromFileRequestBody(
|
||||||
|
file: File,
|
||||||
|
contentType: MediaType?,
|
||||||
|
private val channel: FileChannel,
|
||||||
|
private val chunkSize: Long = CHUNK_SIZE
|
||||||
|
) : FileRequestBody(file, contentType) {
|
||||||
|
|
||||||
|
private var offset: Long = 0
|
||||||
|
private var alreadyTransferred: Long = 0
|
||||||
|
private val buffer = ByteBuffer.allocate(4_096)
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(chunkSize > 0) { "Chunk size must be greater than zero" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentLength(): Long {
|
||||||
|
return chunkSize.coerceAtMost(channel.size() - channel.position())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
var readCount: Int
|
||||||
|
var iterator: Iterator<OnDatatransferProgressListener>
|
||||||
|
try {
|
||||||
|
channel.position(offset)
|
||||||
|
|
||||||
|
val maxCount = (offset + chunkSize).coerceAtMost(channel.size())
|
||||||
|
while (channel.position() < maxCount) {
|
||||||
|
readCount = channel.read(buffer)
|
||||||
|
val bytesToWriteInBuffer = readCount.toLong().coerceAtMost(file.length() - alreadyTransferred).toInt()
|
||||||
|
sink.buffer.write(buffer.array(), 0, bytesToWriteInBuffer)
|
||||||
|
sink.flush()
|
||||||
|
buffer.clear()
|
||||||
|
|
||||||
|
if (alreadyTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks
|
||||||
|
alreadyTransferred += readCount.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
iterator = dataTransferListeners.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
iterator.next().onTransferProgress(readCount.toLong(), alreadyTransferred, file.length(), file.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Timber.e(exception, "Transferred " + alreadyTransferred + " bytes from a total of " + file.length())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOffset(newOffset: Long) {
|
||||||
|
offset = newOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.common.network
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.Source
|
||||||
|
import okio.source
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class ContentUriRequestBody(
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
private val contentUri: Uri
|
||||||
|
) : RequestBody(), ProgressiveDataTransferer {
|
||||||
|
|
||||||
|
private val dataTransferListeners: MutableSet<OnDatatransferProgressListener> = HashSet()
|
||||||
|
|
||||||
|
val fileSize: Long = contentResolver.query(contentUri, null, null, null, null)?.use { cursor ->
|
||||||
|
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getLong(sizeIndex)
|
||||||
|
} ?: -1
|
||||||
|
|
||||||
|
override fun contentType(): MediaType? {
|
||||||
|
val contentType = contentResolver.getType(contentUri) ?: return null
|
||||||
|
return contentType.toMediaTypeOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentLength(): Long {
|
||||||
|
return fileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
val inputStream = contentResolver.openInputStream(contentUri)
|
||||||
|
?: throw IOException("Couldn't open content URI for reading: $contentUri")
|
||||||
|
|
||||||
|
val previousTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
sink.writeAndUpdateProgress(inputStream.source())
|
||||||
|
inputStream.source().close()
|
||||||
|
|
||||||
|
val laterTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
Timber.d("Difference - ${laterTime - previousTime} milliseconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun BufferedSink.writeAndUpdateProgress(source: Source) {
|
||||||
|
var iterator: Iterator<OnDatatransferProgressListener>
|
||||||
|
|
||||||
|
try {
|
||||||
|
var totalBytesRead = 0L
|
||||||
|
var read: Long
|
||||||
|
while (source.read(this.buffer, BYTES_TO_READ).also { read = it } != -1L) {
|
||||||
|
totalBytesRead += read
|
||||||
|
this.flush()
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
iterator = dataTransferListeners.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
iterator.next().onTransferProgress(read, totalBytesRead, fileSize, contentUri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addDatatransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
dataTransferListeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addDatatransferProgressListeners(listeners: MutableCollection<OnDatatransferProgressListener>) {
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
dataTransferListeners.addAll(listeners)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
dataTransferListeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BYTES_TO_READ = 4_096L
|
||||||
|
}
|
||||||
|
}
|
@ -1,119 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.common.network;
|
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import okhttp3.RequestBody;
|
|
||||||
import okio.BufferedSink;
|
|
||||||
import okio.Okio;
|
|
||||||
import okio.Source;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Request body that represents a file and include information about the progress when uploading it
|
|
||||||
*
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
public class FileRequestBody extends RequestBody implements ProgressiveDataTransferer {
|
|
||||||
|
|
||||||
final Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<>();
|
|
||||||
protected File mFile;
|
|
||||||
private MediaType mContentType;
|
|
||||||
|
|
||||||
public FileRequestBody(File file, MediaType contentType) {
|
|
||||||
mFile = file;
|
|
||||||
mContentType = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOneShot() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MediaType contentType() {
|
|
||||||
return mContentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long contentLength() {
|
|
||||||
return mFile.length();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeTo(BufferedSink sink) {
|
|
||||||
Source source;
|
|
||||||
Iterator<OnDatatransferProgressListener> it;
|
|
||||||
try {
|
|
||||||
source = Okio.source(mFile);
|
|
||||||
|
|
||||||
long transferred = 0;
|
|
||||||
long read;
|
|
||||||
|
|
||||||
while ((read = source.read(sink.buffer(), 4096)) != -1) {
|
|
||||||
transferred += read;
|
|
||||||
sink.flush();
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
it = mDataTransferListeners.iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
it.next().onTransferProgress(read, transferred, mFile.length(), mFile.getAbsolutePath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("File with name " + mFile.getName() + " and size " + mFile.length() + " written in request body");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.add(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addDatatransferProgressListeners(Collection<OnDatatransferProgressListener> listeners) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.addAll(listeners);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,97 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.common.network
|
||||||
|
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.BufferedSink
|
||||||
|
import okio.Source
|
||||||
|
import okio.source
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.util.HashSet
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Request body that represents a file and include information about the progress when uploading it
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
*/
|
||||||
|
open class FileRequestBody(
|
||||||
|
val file: File,
|
||||||
|
private val contentType: MediaType?,
|
||||||
|
) : RequestBody(), ProgressiveDataTransferer {
|
||||||
|
|
||||||
|
val dataTransferListeners: MutableSet<OnDatatransferProgressListener> = HashSet()
|
||||||
|
|
||||||
|
override fun isOneShot(): Boolean = true
|
||||||
|
|
||||||
|
override fun contentType(): MediaType? = contentType
|
||||||
|
|
||||||
|
override fun contentLength(): Long = file.length()
|
||||||
|
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
val source: Source
|
||||||
|
var it: Iterator<OnDatatransferProgressListener>
|
||||||
|
try {
|
||||||
|
source = file.source()
|
||||||
|
var transferred: Long = 0
|
||||||
|
var read: Long
|
||||||
|
while (source.read(sink.buffer, BYTES_TO_READ).also { read = it } != -1L) {
|
||||||
|
transferred += read
|
||||||
|
sink.flush()
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
it = dataTransferListeners.iterator()
|
||||||
|
while (it.hasNext()) {
|
||||||
|
it.next().onTransferProgress(read, transferred, file.length(), file.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.d("File with name ${file.name} and size ${file.length()} written in request body")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addDatatransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
dataTransferListeners.add(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addDatatransferProgressListeners(listeners: Collection<OnDatatransferProgressListener>) {
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
dataTransferListeners.addAll(listeners)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
dataTransferListeners.remove(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BYTES_TO_READ = 4_096L
|
||||||
|
}
|
||||||
|
}
|
@ -237,6 +237,10 @@ public class RemoteOperationResult<T>
|
|||||||
httpMethod.getResponseBodyAsString(),
|
httpMethod.getResponseBodyAsString(),
|
||||||
ResultCode.SPECIFIC_METHOD_NOT_ALLOWED
|
ResultCode.SPECIFIC_METHOD_NOT_ALLOWED
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
|
case HttpConstants.HTTP_TOO_EARLY:
|
||||||
|
mCode = ResultCode.TOO_EARLY;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -583,6 +587,7 @@ public class RemoteOperationResult<T>
|
|||||||
SPECIFIC_SERVICE_UNAVAILABLE,
|
SPECIFIC_SERVICE_UNAVAILABLE,
|
||||||
SPECIFIC_UNSUPPORTED_MEDIA_TYPE,
|
SPECIFIC_UNSUPPORTED_MEDIA_TYPE,
|
||||||
SPECIFIC_METHOD_NOT_ALLOWED,
|
SPECIFIC_METHOD_NOT_ALLOWED,
|
||||||
SPECIFIC_BAD_REQUEST
|
SPECIFIC_BAD_REQUEST,
|
||||||
|
TOO_EARLY,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,24 +22,8 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files.chunks;
|
package com.owncloud.android.lib.common.utils
|
||||||
|
|
||||||
import com.owncloud.android.lib.resources.files.CreateRemoteFolderOperation;
|
fun Any.isOneOf(vararg values: Any): Boolean {
|
||||||
|
return this in values
|
||||||
/**
|
}
|
||||||
* Remote operation performing the creation of a new folder to save chunks during an upload to the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
public class CreateRemoteChunkFolderOperation extends CreateRemoteFolderOperation {
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param remotePath Full path to the new directory to create in the remote server.
|
|
||||||
* @param createFullPath 'True' means that all the ancestor folders should be created.
|
|
||||||
*/
|
|
||||||
public CreateRemoteChunkFolderOperation(String remotePath, boolean createFullPath) {
|
|
||||||
super(remotePath, createFullPath);
|
|
||||||
createChunksFolder = true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -36,7 +36,7 @@ data class CommonOcsResponse<T>(
|
|||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class OCSResponse<T>(
|
data class OCSResponse<T>(
|
||||||
val meta: MetaData,
|
val meta: MetaData,
|
||||||
val data: T
|
val data: T?
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.CopyMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation moving a remote file or folder in the ownCloud server to a different folder
|
|
||||||
* in the same account.
|
|
||||||
*
|
|
||||||
* Allows renaming the moving file/folder at the same time.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author Christian Schabesberger
|
|
||||||
* @author David González V.
|
|
||||||
*/
|
|
||||||
public class CopyRemoteFileOperation extends RemoteOperation<String> {
|
|
||||||
|
|
||||||
private static final int COPY_READ_TIMEOUT = 600000;
|
|
||||||
private static final int COPY_CONNECTION_TIMEOUT = 5000;
|
|
||||||
|
|
||||||
private String mSrcRemotePath;
|
|
||||||
private String mTargetRemotePath;
|
|
||||||
|
|
||||||
private boolean mOverwrite;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
* <p/>
|
|
||||||
* TODO Paths should finish in "/" in the case of folders. ?
|
|
||||||
*
|
|
||||||
* @param srcRemotePath Remote path of the file/folder to move.
|
|
||||||
* @param targetRemotePath Remove path desired for the file/folder after moving it.
|
|
||||||
*/
|
|
||||||
public CopyRemoteFileOperation(String srcRemotePath, String targetRemotePath, boolean overwrite
|
|
||||||
) {
|
|
||||||
mSrcRemotePath = srcRemotePath;
|
|
||||||
mTargetRemotePath = targetRemotePath;
|
|
||||||
mOverwrite = overwrite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the rename operation.
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult<String> run(OwnCloudClient client) {
|
|
||||||
|
|
||||||
if (mTargetRemotePath.equals(mSrcRemotePath)) {
|
|
||||||
// nothing to do!
|
|
||||||
return new RemoteOperationResult<>(ResultCode.OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mTargetRemotePath.startsWith(mSrcRemotePath)) {
|
|
||||||
return new RemoteOperationResult<>(ResultCode.INVALID_COPY_INTO_DESCENDANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// perform remote operation
|
|
||||||
RemoteOperationResult result;
|
|
||||||
try {
|
|
||||||
CopyMethod copyMethod =
|
|
||||||
new CopyMethod(
|
|
||||||
new URL(client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mSrcRemotePath)),
|
|
||||||
client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mTargetRemotePath),
|
|
||||||
mOverwrite);
|
|
||||||
|
|
||||||
copyMethod.setReadTimeout(COPY_READ_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
copyMethod.setConnectionTimeout(COPY_CONNECTION_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
final int status = client.executeHttpMethod(copyMethod);
|
|
||||||
|
|
||||||
if (status == HttpConstants.HTTP_CREATED || status == HttpConstants.HTTP_NO_CONTENT) {
|
|
||||||
String fileRemoteId = copyMethod.getResponseHeader(HttpConstants.OC_FILE_REMOTE_ID);
|
|
||||||
result = new RemoteOperationResult<>(ResultCode.OK);
|
|
||||||
result.setData(fileRemoteId);
|
|
||||||
} else if (status == HttpConstants.HTTP_PRECONDITION_FAILED && !mOverwrite) {
|
|
||||||
result = new RemoteOperationResult<>(ResultCode.INVALID_OVERWRITE);
|
|
||||||
client.exhaustResponse(copyMethod.getResponseBodyAsStream());
|
|
||||||
|
|
||||||
/// for other errors that could be explicitly handled, check first:
|
|
||||||
/// http://www.webdav.org/specs/rfc4918.html#rfc.section.9.9.4
|
|
||||||
} else {
|
|
||||||
|
|
||||||
result = new RemoteOperationResult<>(copyMethod);
|
|
||||||
client.exhaustResponse(copyMethod.getResponseBodyAsStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Copy " + mSrcRemotePath + " to " + mTargetRemotePath + ": " + result.getLogMessage());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Copy " + mSrcRemotePath + " to " + mTargetRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,114 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.CopyMethod
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation copying a remote file or folder in the ownCloud server to a different folder
|
||||||
|
* in the same account.
|
||||||
|
*
|
||||||
|
* Allows renaming the copying file/folder at the same time.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author Christian Schabesberger
|
||||||
|
* @author David González V.
|
||||||
|
*
|
||||||
|
* @param srcRemotePath Remote path of the file/folder to copy.
|
||||||
|
* @param targetRemotePath Remote path desired for the file/folder to copy it.
|
||||||
|
*/
|
||||||
|
class CopyRemoteFileOperation(
|
||||||
|
private val srcRemotePath: String,
|
||||||
|
private val targetRemotePath: String,
|
||||||
|
) : RemoteOperation<String>() {
|
||||||
|
/**
|
||||||
|
* Performs the rename operation.
|
||||||
|
*
|
||||||
|
* @param client Client object to communicate with the remote ownCloud server.
|
||||||
|
*/
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<String> {
|
||||||
|
if (targetRemotePath == srcRemotePath) {
|
||||||
|
// nothing to do!
|
||||||
|
return RemoteOperationResult(ResultCode.OK)
|
||||||
|
}
|
||||||
|
if (targetRemotePath.startsWith(srcRemotePath)) {
|
||||||
|
return RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// perform remote operation
|
||||||
|
var result: RemoteOperationResult<String>
|
||||||
|
try {
|
||||||
|
val copyMethod = CopyMethod(
|
||||||
|
URL(client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(srcRemotePath)),
|
||||||
|
client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(targetRemotePath),
|
||||||
|
).apply {
|
||||||
|
setReadTimeout(COPY_READ_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
setConnectionTimeout(COPY_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
val status = client.executeHttpMethod(copyMethod)
|
||||||
|
when {
|
||||||
|
isSuccess(status) -> {
|
||||||
|
val fileRemoteId = copyMethod.getResponseHeader(HttpConstants.OC_FILE_REMOTE_ID)
|
||||||
|
result = RemoteOperationResult(ResultCode.OK)
|
||||||
|
result.setData(fileRemoteId)
|
||||||
|
}
|
||||||
|
isPreconditionFailed(status) -> {
|
||||||
|
result = RemoteOperationResult(ResultCode.INVALID_OVERWRITE)
|
||||||
|
client.exhaustResponse(copyMethod.getResponseBodyAsStream())
|
||||||
|
|
||||||
|
/// for other errors that could be explicitly handled, check first:
|
||||||
|
/// http://www.webdav.org/specs/rfc4918.html#rfc.section.9.9.4
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result = RemoteOperationResult(copyMethod)
|
||||||
|
client.exhaustResponse(copyMethod.getResponseBodyAsStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.i("Copy $srcRemotePath to $targetRemotePath: ${result.logMessage}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result = RemoteOperationResult(e)
|
||||||
|
Timber.e(e, "Copy $srcRemotePath to $targetRemotePath: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int) = status.isOneOf(HttpConstants.HTTP_CREATED, HttpConstants.HTTP_NO_CONTENT)
|
||||||
|
|
||||||
|
private fun isPreconditionFailed(status: Int) = status == HttpConstants.HTTP_PRECONDITION_FAILED
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val COPY_READ_TIMEOUT = 10L
|
||||||
|
private const val COPY_CONNECTION_TIMEOUT = 6L
|
||||||
|
}
|
||||||
|
}
|
@ -1,114 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2019 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.MkColMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the creation of a new folder in the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
*/
|
|
||||||
public class CreateRemoteFolderOperation extends RemoteOperation {
|
|
||||||
|
|
||||||
private static final int READ_TIMEOUT = 30000;
|
|
||||||
private static final int CONNECTION_TIMEOUT = 5000;
|
|
||||||
|
|
||||||
private String mRemotePath;
|
|
||||||
private boolean mCreateFullPath;
|
|
||||||
protected boolean createChunksFolder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param remotePath Full path to the new directory to create in the remote server.
|
|
||||||
* @param createFullPath 'True' means that all the ancestor folders should be created.
|
|
||||||
*/
|
|
||||||
public CreateRemoteFolderOperation(String remotePath, boolean createFullPath) {
|
|
||||||
mRemotePath = remotePath;
|
|
||||||
mCreateFullPath = createFullPath;
|
|
||||||
createChunksFolder = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the operation
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult run(OwnCloudClient client) {
|
|
||||||
RemoteOperationResult result = createFolder(client);
|
|
||||||
if (!result.isSuccess() && mCreateFullPath &&
|
|
||||||
RemoteOperationResult.ResultCode.CONFLICT == result.getCode()) {
|
|
||||||
result = createParentFolder(FileUtils.getParentPath(mRemotePath), client);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
result = createFolder(client); // second (and last) try
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RemoteOperationResult createFolder(OwnCloudClient client) {
|
|
||||||
RemoteOperationResult result;
|
|
||||||
try {
|
|
||||||
Uri webDavUri = createChunksFolder ? client.getUploadsWebDavUri() : client.getUserFilesWebDavUri();
|
|
||||||
final MkColMethod mkcol = new MkColMethod(
|
|
||||||
new URL(webDavUri + WebdavUtils.encodePath(mRemotePath)));
|
|
||||||
mkcol.setReadTimeout(READ_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
mkcol.setConnectionTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
final int status = client.executeHttpMethod(mkcol);
|
|
||||||
|
|
||||||
result = (status == HttpConstants.HTTP_CREATED)
|
|
||||||
? new RemoteOperationResult<>(ResultCode.OK)
|
|
||||||
: new RemoteOperationResult<>(mkcol);
|
|
||||||
Timber.d("Create directory " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
client.exhaustResponse(mkcol.getResponseBodyAsStream());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Create directory " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RemoteOperationResult createParentFolder(String parentPath, OwnCloudClient client) {
|
|
||||||
RemoteOperation operation = new CreateRemoteFolderOperation(parentPath, mCreateFullPath);
|
|
||||||
return operation.execute(client);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,109 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.MkColMethod
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the creation of a new folder in the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
*
|
||||||
|
* @param remotePath Full path to the new directory to create in the remote server.
|
||||||
|
* @param createFullPath 'True' means that all the ancestor folders should be created.
|
||||||
|
*/
|
||||||
|
class CreateRemoteFolderOperation(
|
||||||
|
val remotePath: String,
|
||||||
|
private val createFullPath: Boolean,
|
||||||
|
private val isChunksFolder: Boolean = false
|
||||||
|
) : RemoteOperation<Unit>() {
|
||||||
|
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
|
||||||
|
var result = createFolder(client)
|
||||||
|
if (!result.isSuccess && createFullPath && result.code == ResultCode.CONFLICT) {
|
||||||
|
result = createParentFolder(FileUtils.getParentPath(remotePath), client)
|
||||||
|
|
||||||
|
if (result.isSuccess) {
|
||||||
|
// Second and last try
|
||||||
|
result = createFolder(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFolder(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
var result: RemoteOperationResult<Unit>
|
||||||
|
try {
|
||||||
|
val webDavUri = if (isChunksFolder) {
|
||||||
|
client.uploadsWebDavUri
|
||||||
|
} else {
|
||||||
|
client.userFilesWebDavUri
|
||||||
|
}
|
||||||
|
|
||||||
|
val mkCol = MkColMethod(
|
||||||
|
URL(webDavUri.toString() + WebdavUtils.encodePath(remotePath))
|
||||||
|
).apply {
|
||||||
|
setReadTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
setConnectionTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = client.executeHttpMethod(mkCol)
|
||||||
|
result =
|
||||||
|
if (status == HttpConstants.HTTP_CREATED) {
|
||||||
|
RemoteOperationResult(ResultCode.OK)
|
||||||
|
} else {
|
||||||
|
RemoteOperationResult(mkCol)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Create directory $remotePath: ${result.logMessage}")
|
||||||
|
client.exhaustResponse(mkCol.getResponseBodyAsStream())
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result = RemoteOperationResult(e)
|
||||||
|
Timber.e(e, "Create directory $remotePath: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createParentFolder(parentPath: String, client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
val operation: RemoteOperation<Unit> = CreateRemoteFolderOperation(parentPath, createFullPath)
|
||||||
|
return operation.execute(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val READ_TIMEOUT: Long = 30_000
|
||||||
|
private const val CONNECTION_TIMEOUT: Long = 5_000
|
||||||
|
}
|
||||||
|
}
|
@ -1,221 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2016 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.OperationCancelledException;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the download of a remote file in the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class DownloadRemoteFileOperation extends RemoteOperation {
|
|
||||||
|
|
||||||
private static final int FORBIDDEN_ERROR = 403;
|
|
||||||
private static final int SERVICE_UNAVAILABLE_ERROR = 503;
|
|
||||||
private final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
|
|
||||||
private Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<>();
|
|
||||||
private long mModificationTimestamp = 0;
|
|
||||||
private String mEtag = "";
|
|
||||||
private GetMethod mGet;
|
|
||||||
|
|
||||||
private String mRemotePath;
|
|
||||||
private String mLocalFolderPath;
|
|
||||||
|
|
||||||
public DownloadRemoteFileOperation(String remotePath, String localFolderPath) {
|
|
||||||
mRemotePath = remotePath;
|
|
||||||
mLocalFolderPath = localFolderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult run(OwnCloudClient client) {
|
|
||||||
RemoteOperationResult result;
|
|
||||||
|
|
||||||
/// download will be performed to a temporal file, then moved to the final location
|
|
||||||
File tmpFile = new File(getTmpPath());
|
|
||||||
|
|
||||||
/// perform the download
|
|
||||||
try {
|
|
||||||
tmpFile.getParentFile().mkdirs();
|
|
||||||
result = downloadFile(client, tmpFile);
|
|
||||||
Timber.i("Download of " + mRemotePath + " to " + getTmpPath() + ": " + result.getLogMessage());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Download of " + mRemotePath + " to " + getTmpPath() + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RemoteOperationResult downloadFile(OwnCloudClient client, File targetFile) throws
|
|
||||||
Exception {
|
|
||||||
|
|
||||||
RemoteOperationResult result;
|
|
||||||
int status;
|
|
||||||
boolean savedFile = false;
|
|
||||||
mGet = new GetMethod(new URL(client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mRemotePath)));
|
|
||||||
Iterator<OnDatatransferProgressListener> it;
|
|
||||||
|
|
||||||
FileOutputStream fos = null;
|
|
||||||
BufferedInputStream bis = null;
|
|
||||||
try {
|
|
||||||
status = client.executeHttpMethod(mGet);
|
|
||||||
if (isSuccess(status)) {
|
|
||||||
targetFile.createNewFile();
|
|
||||||
bis = new BufferedInputStream(mGet.getResponseBodyAsStream());
|
|
||||||
fos = new FileOutputStream(targetFile);
|
|
||||||
long transferred = 0;
|
|
||||||
|
|
||||||
String contentLength = mGet.getResponseHeader(HttpConstants.CONTENT_LENGTH_HEADER);
|
|
||||||
long totalToTransfer =
|
|
||||||
(contentLength != null
|
|
||||||
&& contentLength.length() > 0)
|
|
||||||
? Long.parseLong(contentLength)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
byte[] bytes = new byte[4096];
|
|
||||||
int readResult;
|
|
||||||
while ((readResult = bis.read(bytes)) != -1) {
|
|
||||||
synchronized (mCancellationRequested) {
|
|
||||||
if (mCancellationRequested.get()) {
|
|
||||||
mGet.abort();
|
|
||||||
throw new OperationCancelledException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fos.write(bytes, 0, readResult);
|
|
||||||
transferred += readResult;
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
it = mDataTransferListeners.iterator();
|
|
||||||
while (it.hasNext()) {
|
|
||||||
it.next().onTransferProgress(readResult, transferred, totalToTransfer,
|
|
||||||
targetFile.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (transferred == totalToTransfer) { // Check if the file is completed
|
|
||||||
savedFile = true;
|
|
||||||
final String modificationTime =
|
|
||||||
mGet.getResponseHeaders().get("Last-Modified") != null
|
|
||||||
? mGet.getResponseHeaders().get("Last-Modified")
|
|
||||||
: mGet.getResponseHeader("last-modified");
|
|
||||||
|
|
||||||
if (modificationTime != null) {
|
|
||||||
final Date d = WebdavUtils.parseResponseDate(modificationTime);
|
|
||||||
mModificationTimestamp = (d != null) ? d.getTime() : 0;
|
|
||||||
} else {
|
|
||||||
Timber.e("Could not read modification time from response downloading %s", mRemotePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
mEtag = WebdavUtils.getEtagFromResponse(mGet);
|
|
||||||
|
|
||||||
// Get rid of extra quotas
|
|
||||||
mEtag = mEtag.replace("\"", "");
|
|
||||||
|
|
||||||
if (mEtag.length() == 0) {
|
|
||||||
Timber.e("Could not read eTag from response downloading %s", mRemotePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Timber.e("Content-Length not equal to transferred bytes.");
|
|
||||||
Timber.d("totalToTransfer = %d, transferred = %d", totalToTransfer, transferred);
|
|
||||||
client.exhaustResponse(mGet.getResponseBodyAsStream());
|
|
||||||
// TODO some kind of error control!
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (status != FORBIDDEN_ERROR && status != SERVICE_UNAVAILABLE_ERROR) {
|
|
||||||
client.exhaustResponse(mGet.getResponseBodyAsStream());
|
|
||||||
|
|
||||||
} // else, body read by RemoteOperationResult constructor
|
|
||||||
|
|
||||||
result = isSuccess(status)
|
|
||||||
? new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK)
|
|
||||||
: new RemoteOperationResult<>(mGet);
|
|
||||||
} finally {
|
|
||||||
if (fos != null) {
|
|
||||||
fos.close();
|
|
||||||
}
|
|
||||||
if (bis != null) {
|
|
||||||
bis.close();
|
|
||||||
}
|
|
||||||
if (!savedFile && targetFile.exists()) {
|
|
||||||
targetFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSuccess(int status) {
|
|
||||||
return (status == HttpConstants.HTTP_OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getTmpPath() {
|
|
||||||
return mLocalFolderPath + mRemotePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.add(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancel() {
|
|
||||||
mCancellationRequested.set(true); // atomic set; there is no need of synchronizing it
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getModificationTimestamp() {
|
|
||||||
return mModificationTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEtag() {
|
|
||||||
return mEtag;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,184 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod
|
||||||
|
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.OperationCancelledException
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.HashSet
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the download of a remote file in the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
*/
|
||||||
|
class DownloadRemoteFileOperation(
|
||||||
|
private val remotePath: String,
|
||||||
|
localFolderPath: String
|
||||||
|
) : RemoteOperation<Unit>() {
|
||||||
|
|
||||||
|
private val cancellationRequested = AtomicBoolean(false)
|
||||||
|
private val dataTransferListeners: MutableSet<OnDatatransferProgressListener> = HashSet()
|
||||||
|
|
||||||
|
var modificationTimestamp: Long = 0
|
||||||
|
private set
|
||||||
|
|
||||||
|
var etag: String = ""
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
// download will be performed to a temporal file, then moved to the final location
|
||||||
|
val tmpFile = File(tmpPath)
|
||||||
|
|
||||||
|
// perform the download
|
||||||
|
return try {
|
||||||
|
tmpFile.parentFile?.mkdirs()
|
||||||
|
downloadFile(client, tmpFile).also { result ->
|
||||||
|
Timber.i("Download of $remotePath to $tmpPath: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
RemoteOperationResult<Unit>(e).also { result ->
|
||||||
|
Timber.e(e, "Download of $remotePath to $tmpPath: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun downloadFile(client: OwnCloudClient, targetFile: File): RemoteOperationResult<Unit> {
|
||||||
|
val result: RemoteOperationResult<Unit>
|
||||||
|
var it: Iterator<OnDatatransferProgressListener>
|
||||||
|
var fos: FileOutputStream? = null
|
||||||
|
var bis: BufferedInputStream? = null
|
||||||
|
var savedFile = false
|
||||||
|
|
||||||
|
val getMethod = GetMethod(URL(client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath)))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val status = client.executeHttpMethod(getMethod)
|
||||||
|
|
||||||
|
if (isSuccess(status)) {
|
||||||
|
targetFile.createNewFile()
|
||||||
|
bis = BufferedInputStream(getMethod.getResponseBodyAsStream())
|
||||||
|
fos = FileOutputStream(targetFile)
|
||||||
|
var transferred: Long = 0
|
||||||
|
val contentLength = getMethod.getResponseHeader(HttpConstants.CONTENT_LENGTH_HEADER)
|
||||||
|
val totalToTransfer = if (!contentLength.isNullOrEmpty()) {
|
||||||
|
contentLength.toLong()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
val bytes = ByteArray(4096)
|
||||||
|
var readResult: Int
|
||||||
|
while (bis.read(bytes).also { readResult = it } != -1) {
|
||||||
|
synchronized(cancellationRequested) {
|
||||||
|
if (cancellationRequested.get()) {
|
||||||
|
getMethod.abort()
|
||||||
|
throw OperationCancelledException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fos.write(bytes, 0, readResult)
|
||||||
|
transferred += readResult.toLong()
|
||||||
|
synchronized(dataTransferListeners) {
|
||||||
|
it = dataTransferListeners.iterator()
|
||||||
|
while (it.hasNext()) {
|
||||||
|
it.next()
|
||||||
|
.onTransferProgress(readResult.toLong(), transferred, totalToTransfer, targetFile.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transferred == totalToTransfer) { // Check if the file is completed
|
||||||
|
savedFile = true
|
||||||
|
val modificationTime =
|
||||||
|
getMethod.getResponseHeaders()?.get("Last-Modified")
|
||||||
|
?: getMethod.getResponseHeader("last-modified")
|
||||||
|
|
||||||
|
if (modificationTime != null) {
|
||||||
|
val modificationDate = WebdavUtils.parseResponseDate(modificationTime)
|
||||||
|
modificationTimestamp = modificationDate?.time ?: 0
|
||||||
|
} else {
|
||||||
|
Timber.e("Could not read modification time from response downloading %s", remotePath)
|
||||||
|
}
|
||||||
|
etag = WebdavUtils.getEtagFromResponse(getMethod)
|
||||||
|
|
||||||
|
// Get rid of extra quotas
|
||||||
|
etag = etag.replace("\"", "")
|
||||||
|
if (etag.isEmpty()) {
|
||||||
|
Timber.e("Could not read eTag from response downloading %s", remotePath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.e("Content-Length not equal to transferred bytes.")
|
||||||
|
Timber.d("totalToTransfer = $totalToTransfer, transferred = $transferred")
|
||||||
|
client.exhaustResponse(getMethod.getResponseBodyAsStream())
|
||||||
|
// TODO some kind of error control!
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (status != HttpConstants.HTTP_FORBIDDEN && status != HttpConstants.HTTP_SERVICE_UNAVAILABLE) {
|
||||||
|
client.exhaustResponse(getMethod.getResponseBodyAsStream())
|
||||||
|
} // else, body read by RemoteOperationResult constructor
|
||||||
|
|
||||||
|
result =
|
||||||
|
if (isSuccess(status)) {
|
||||||
|
RemoteOperationResult(RemoteOperationResult.ResultCode.OK)
|
||||||
|
} else {
|
||||||
|
RemoteOperationResult(getMethod)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fos?.close()
|
||||||
|
bis?.close()
|
||||||
|
if (!savedFile && targetFile.exists()) {
|
||||||
|
targetFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int) = status == HttpConstants.HTTP_OK
|
||||||
|
|
||||||
|
private val tmpPath: String = localFolderPath + remotePath
|
||||||
|
|
||||||
|
fun addDatatransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListeners) { dataTransferListeners.add(listener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeDatatransferProgressListener(listener: OnDatatransferProgressListener?) {
|
||||||
|
synchronized(dataTransferListeners) { dataTransferListeners.remove(listener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
cancellationRequested.set(true) // atomic set; there is no need of synchronizing it
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ public class FileUtils {
|
|||||||
public static final String FINAL_CHUNKS_FILE = ".file";
|
public static final String FINAL_CHUNKS_FILE = ".file";
|
||||||
public static final String MIME_DIR = "DIR";
|
public static final String MIME_DIR = "DIR";
|
||||||
public static final String MIME_DIR_UNIX = "httpd/unix-directory";
|
public static final String MIME_DIR_UNIX = "httpd/unix-directory";
|
||||||
|
public static final String MODE_READ_ONLY = "r";
|
||||||
|
|
||||||
static String getParentPath(String remotePath) {
|
static String getParentPath(String remotePath) {
|
||||||
String parentPath = new File(remotePath).getParent();
|
String parentPath = new File(remotePath).getParent();
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.MoveMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation moving a remote file or folder in the ownCloud server to a different folder
|
|
||||||
* in the same account.
|
|
||||||
* <p>
|
|
||||||
* Allows renaming the moving file/folder at the same time.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
public class MoveRemoteFileOperation extends RemoteOperation {
|
|
||||||
|
|
||||||
private static final int MOVE_READ_TIMEOUT = 600000;
|
|
||||||
private static final int MOVE_CONNECTION_TIMEOUT = 5000;
|
|
||||||
|
|
||||||
private String mSrcRemotePath;
|
|
||||||
private String mTargetRemotePath;
|
|
||||||
private boolean mOverwrite;
|
|
||||||
|
|
||||||
protected boolean moveChunkedFile = false;
|
|
||||||
protected String mFileLastModifTimestamp;
|
|
||||||
protected long mFileLength;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
* <p>
|
|
||||||
* TODO Paths should finish in "/" in the case of folders. ?
|
|
||||||
*
|
|
||||||
* @param srcRemotePath Remote path of the file/folder to move.
|
|
||||||
* @param targetRemotePath Remote path desired for the file/folder after moving it.
|
|
||||||
*/
|
|
||||||
public MoveRemoteFileOperation(String srcRemotePath,
|
|
||||||
String targetRemotePath,
|
|
||||||
boolean overwrite) {
|
|
||||||
|
|
||||||
mSrcRemotePath = srcRemotePath;
|
|
||||||
mTargetRemotePath = targetRemotePath;
|
|
||||||
mOverwrite = overwrite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the rename operation.
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult run(OwnCloudClient client) {
|
|
||||||
if (mTargetRemotePath.equals(mSrcRemotePath)) {
|
|
||||||
// nothing to do!
|
|
||||||
return new RemoteOperationResult<>(ResultCode.OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mTargetRemotePath.startsWith(mSrcRemotePath)) {
|
|
||||||
return new RemoteOperationResult<>(ResultCode.INVALID_MOVE_INTO_DESCENDANT);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// perform remote operation
|
|
||||||
RemoteOperationResult result;
|
|
||||||
try {
|
|
||||||
// After finishing a chunked upload, we have to move the resulting file from uploads folder to files one,
|
|
||||||
// so this uri has to be customizable
|
|
||||||
Uri srcWebDavUri = moveChunkedFile ? client.getUploadsWebDavUri() : client.getUserFilesWebDavUri();
|
|
||||||
|
|
||||||
final MoveMethod move = new MoveMethod(
|
|
||||||
new URL(srcWebDavUri + WebdavUtils.encodePath(mSrcRemotePath)),
|
|
||||||
client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mTargetRemotePath),
|
|
||||||
mOverwrite);
|
|
||||||
|
|
||||||
if (moveChunkedFile) {
|
|
||||||
move.addRequestHeader(HttpConstants.OC_X_OC_MTIME_HEADER, mFileLastModifTimestamp);
|
|
||||||
move.addRequestHeader(HttpConstants.OC_TOTAL_LENGTH_HEADER, String.valueOf(mFileLength));
|
|
||||||
}
|
|
||||||
|
|
||||||
move.setReadTimeout(MOVE_READ_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
move.setConnectionTimeout(MOVE_CONNECTION_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
final int status = client.executeHttpMethod(move);
|
|
||||||
/// process response
|
|
||||||
if (isSuccess(status)) {
|
|
||||||
result = new RemoteOperationResult<>(ResultCode.OK);
|
|
||||||
} else if (status == HttpConstants.HTTP_PRECONDITION_FAILED && !mOverwrite) {
|
|
||||||
|
|
||||||
result = new RemoteOperationResult<>(ResultCode.INVALID_OVERWRITE);
|
|
||||||
client.exhaustResponse(move.getResponseBodyAsStream());
|
|
||||||
|
|
||||||
/// for other errors that could be explicitly handled, check first:
|
|
||||||
/// http://www.webdav.org/specs/rfc4918.html#rfc.section.9.9.4
|
|
||||||
|
|
||||||
} else {
|
|
||||||
result = new RemoteOperationResult<>(move);
|
|
||||||
client.exhaustResponse(move.getResponseBodyAsStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Move " + mSrcRemotePath + " to " + mTargetRemotePath + ": " + result.getLogMessage());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Move " + mSrcRemotePath + " to " + mTargetRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isSuccess(int status) {
|
|
||||||
return status == HttpConstants.HTTP_CREATED || status == HttpConstants.HTTP_NO_CONTENT;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,133 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.MoveMethod
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation moving a remote file or folder in the ownCloud server to a different folder
|
||||||
|
* in the same account.
|
||||||
|
*
|
||||||
|
* Allows renaming the moving file/folder at the same time.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*/
|
||||||
|
open class MoveRemoteFileOperation(
|
||||||
|
private val sourceRemotePath: String,
|
||||||
|
private val targetRemotePath: String,
|
||||||
|
) : RemoteOperation<Unit>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the rename operation.
|
||||||
|
*
|
||||||
|
* @param client Client object to communicate with the remote ownCloud server.
|
||||||
|
*/
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
if (targetRemotePath == sourceRemotePath) {
|
||||||
|
// nothing to do!
|
||||||
|
return RemoteOperationResult(ResultCode.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetRemotePath.startsWith(sourceRemotePath)) {
|
||||||
|
return RemoteOperationResult(ResultCode.INVALID_MOVE_INTO_DESCENDANT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// perform remote operation
|
||||||
|
var result: RemoteOperationResult<Unit>
|
||||||
|
try {
|
||||||
|
// After finishing a chunked upload, we have to move the resulting file from uploads folder to files one,
|
||||||
|
// so this uri has to be customizable
|
||||||
|
val srcWebDavUri = getSrcWebDavUriForClient(client)
|
||||||
|
val moveMethod = MoveMethod(
|
||||||
|
url = URL(srcWebDavUri.toString() + WebdavUtils.encodePath(sourceRemotePath)),
|
||||||
|
destinationUrl = client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(targetRemotePath),
|
||||||
|
).apply {
|
||||||
|
addRequestHeaders(this)
|
||||||
|
setReadTimeout(MOVE_READ_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
setConnectionTimeout(MOVE_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = client.executeHttpMethod(moveMethod)
|
||||||
|
|
||||||
|
when {
|
||||||
|
isSuccess(status) -> {
|
||||||
|
result = RemoteOperationResult<Unit>(ResultCode.OK)
|
||||||
|
}
|
||||||
|
isPreconditionFailed(status) -> {
|
||||||
|
result = RemoteOperationResult<Unit>(ResultCode.INVALID_OVERWRITE)
|
||||||
|
client.exhaustResponse(moveMethod.getResponseBodyAsStream())
|
||||||
|
|
||||||
|
/// for other errors that could be explicitly handled, check first:
|
||||||
|
/// http://www.webdav.org/specs/rfc4918.html#rfc.section.9.9.4
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result = RemoteOperationResult<Unit>(moveMethod)
|
||||||
|
client.exhaustResponse(moveMethod.getResponseBodyAsStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.i("Move $sourceRemotePath to $targetRemotePath: ${result.logMessage}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result = RemoteOperationResult<Unit>(e)
|
||||||
|
Timber.e(e, "Move $sourceRemotePath to $targetRemotePath: ${result.logMessage}")
|
||||||
|
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For standard moves, we will use [OwnCloudClient.getUserFilesWebDavUri].
|
||||||
|
* In case we need a different source Uri, override this method.
|
||||||
|
*/
|
||||||
|
open fun getSrcWebDavUriForClient(client: OwnCloudClient): Uri = client.userFilesWebDavUri
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For standard moves, we won't need any special headers.
|
||||||
|
* In case new headers are needed, override this method
|
||||||
|
*/
|
||||||
|
open fun addRequestHeaders(moveMethod: MoveMethod) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int) = status.isOneOf(HttpConstants.HTTP_CREATED, HttpConstants.HTTP_NO_CONTENT)
|
||||||
|
|
||||||
|
private fun isPreconditionFailed(status: Int) = status == HttpConstants.HTTP_PRECONDITION_FAILED
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MOVE_READ_TIMEOUT = 10L
|
||||||
|
private const val MOVE_CONNECTION_TIMEOUT = 6L
|
||||||
|
}
|
||||||
|
}
|
@ -1,108 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2016 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.accounts.AccountUtils;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.DavUtils;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.owncloud.android.lib.common.http.methods.webdav.DavConstants.DEPTH_0;
|
|
||||||
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the read a file from the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ReadRemoteFileOperation extends RemoteOperation<RemoteFile> {
|
|
||||||
|
|
||||||
private static final int SYNC_READ_TIMEOUT = 40000;
|
|
||||||
private static final int SYNC_CONNECTION_TIMEOUT = 5000;
|
|
||||||
|
|
||||||
private String mRemotePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param remotePath Remote path of the file.
|
|
||||||
*/
|
|
||||||
public ReadRemoteFileOperation(String remotePath) {
|
|
||||||
mRemotePath = remotePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the read operation.
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult<RemoteFile> run(OwnCloudClient client) {
|
|
||||||
PropfindMethod propfind;
|
|
||||||
RemoteOperationResult<RemoteFile> result;
|
|
||||||
|
|
||||||
/// take the duty of check the server for the current state of the file there
|
|
||||||
try {
|
|
||||||
// remote request
|
|
||||||
propfind = new PropfindMethod(
|
|
||||||
new URL(client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mRemotePath)),
|
|
||||||
DEPTH_0,
|
|
||||||
DavUtils.getAllPropset());
|
|
||||||
|
|
||||||
propfind.setReadTimeout(SYNC_READ_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
propfind.setConnectionTimeout(SYNC_CONNECTION_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
final int status = client.executeHttpMethod(propfind);
|
|
||||||
|
|
||||||
if (status == HttpConstants.HTTP_MULTI_STATUS
|
|
||||||
|| status == HttpConstants.HTTP_OK) {
|
|
||||||
|
|
||||||
final RemoteFile file = new RemoteFile(propfind.getRoot(), AccountUtils.getUserId(mAccount, mContext));
|
|
||||||
|
|
||||||
result = new RemoteOperationResult<>(OK);
|
|
||||||
result.setData(file);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
result = new RemoteOperationResult<>(propfind);
|
|
||||||
client.exhaustResponse(propfind.getResponseBodyAsStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Synchronizing file %s", mRemotePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,97 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2016 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.accounts.AccountUtils
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants.HTTP_MULTI_STATUS
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants.HTTP_OK
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.DavConstants.DEPTH_0
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.DavUtils
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the read a file from the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
* @author David González Verdugo
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ReadRemoteFileOperation(val remotePath: String) : RemoteOperation<RemoteFile>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the read operation.
|
||||||
|
*
|
||||||
|
* @param client Client object to communicate with the remote ownCloud server.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<RemoteFile> {
|
||||||
|
try {
|
||||||
|
val propFind = PropfindMethod(
|
||||||
|
url = URL("${client.userFilesWebDavUri}${WebdavUtils.encodePath(remotePath)}"),
|
||||||
|
depth = DEPTH_0,
|
||||||
|
propertiesToRequest = DavUtils.allPropset
|
||||||
|
).apply {
|
||||||
|
setReadTimeout(SYNC_READ_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
setConnectionTimeout(SYNC_CONNECTION_TIMEOUT, TimeUnit.SECONDS)
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = client.executeHttpMethod(propFind)
|
||||||
|
Timber.i("Read remote file $remotePath with status ${propFind.statusCode}")
|
||||||
|
|
||||||
|
return if (isSuccess(status)) {
|
||||||
|
// TODO: Remove that !!
|
||||||
|
val remoteFile = RemoteFile.getRemoteFileFromDav(
|
||||||
|
propFind.root!!,
|
||||||
|
AccountUtils.getUserId(mAccount, mContext), mAccount.name
|
||||||
|
)
|
||||||
|
|
||||||
|
RemoteOperationResult<RemoteFile>(RemoteOperationResult.ResultCode.OK).apply {
|
||||||
|
data = remoteFile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RemoteOperationResult<RemoteFile>(propFind).also {
|
||||||
|
client.exhaustResponse(propFind.getResponseBodyAsStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
return RemoteOperationResult(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int) = status.isOneOf(HTTP_MULTI_STATUS, HTTP_OK)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SYNC_READ_TIMEOUT = 40_000L
|
||||||
|
private const val SYNC_CONNECTION_TIMEOUT = 5_000L
|
||||||
|
}
|
||||||
|
}
|
@ -1,128 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import at.bitfire.dav4jvm.PropertyRegistry;
|
|
||||||
import at.bitfire.dav4jvm.Response;
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.accounts.AccountUtils;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.DavConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.DavUtils;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.properties.OCShareTypes;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the read of remote file or folder in the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ReadRemoteFolderOperation extends RemoteOperation<ArrayList<RemoteFile>> {
|
|
||||||
|
|
||||||
private String mRemotePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param remotePath Remote path of the file.
|
|
||||||
*/
|
|
||||||
public ReadRemoteFolderOperation(String remotePath) {
|
|
||||||
mRemotePath = remotePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the read operation.
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult<ArrayList<RemoteFile>> run(OwnCloudClient client) {
|
|
||||||
RemoteOperationResult<ArrayList<RemoteFile>> result = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
PropertyRegistry.INSTANCE.register(OCShareTypes.Factory.class.newInstance());
|
|
||||||
PropfindMethod propfindMethod = new PropfindMethod(
|
|
||||||
new URL(client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mRemotePath)),
|
|
||||||
DavConstants.DEPTH_1,
|
|
||||||
DavUtils.getAllPropset());
|
|
||||||
|
|
||||||
int status = client.executeHttpMethod(propfindMethod);
|
|
||||||
|
|
||||||
if (isSuccess(status)) {
|
|
||||||
ArrayList<RemoteFile> mFolderAndFiles = new ArrayList<>();
|
|
||||||
|
|
||||||
// parse data from remote folder
|
|
||||||
mFolderAndFiles.add(
|
|
||||||
new RemoteFile(propfindMethod.getRoot(), AccountUtils.getUserId(mAccount, mContext))
|
|
||||||
);
|
|
||||||
|
|
||||||
// loop to update every child
|
|
||||||
for (Response resource : propfindMethod.getMembers()) {
|
|
||||||
RemoteFile file = new RemoteFile(resource, AccountUtils.getUserId(mAccount, mContext));
|
|
||||||
mFolderAndFiles.add(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Result of the operation
|
|
||||||
result = new RemoteOperationResult<>(OK);
|
|
||||||
result.setData(mFolderAndFiles);
|
|
||||||
|
|
||||||
} else { // synchronization failed
|
|
||||||
result = new RemoteOperationResult<>(propfindMethod);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
} finally {
|
|
||||||
if (result == null) {
|
|
||||||
Timber.e("Synchronized " + mRemotePath + ": result is null");
|
|
||||||
} else if (result.isSuccess()) {
|
|
||||||
Timber.i("Synchronized " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
} else {
|
|
||||||
if (result.isException()) {
|
|
||||||
Timber.e(result.getException(), "Synchronized " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
} else {
|
|
||||||
Timber.e("Synchronized " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSuccess(int status) {
|
|
||||||
return status == HttpConstants.HTTP_MULTI_STATUS || status == HttpConstants.HTTP_OK;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,111 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import at.bitfire.dav4jvm.PropertyRegistry
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.accounts.AccountUtils
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants.HTTP_MULTI_STATUS
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants.HTTP_OK
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.DavConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.DavUtils
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.PropfindMethod
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.properties.OCShareTypes
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the read of remote file or folder in the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
* @author David González Verdugo
|
||||||
|
*/
|
||||||
|
class ReadRemoteFolderOperation(
|
||||||
|
val remotePath: String
|
||||||
|
) : RemoteOperation<ArrayList<RemoteFile>>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the read operation.
|
||||||
|
*
|
||||||
|
* @param client Client object to communicate with the remote ownCloud server.
|
||||||
|
*/
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<ArrayList<RemoteFile>> {
|
||||||
|
try {
|
||||||
|
PropertyRegistry.register(OCShareTypes.Factory())
|
||||||
|
|
||||||
|
val propfindMethod = PropfindMethod(
|
||||||
|
URL(client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath)),
|
||||||
|
DavConstants.DEPTH_1,
|
||||||
|
DavUtils.allPropset
|
||||||
|
)
|
||||||
|
|
||||||
|
val status = client.executeHttpMethod(propfindMethod)
|
||||||
|
|
||||||
|
if (isSuccess(status)) {
|
||||||
|
val mFolderAndFiles = ArrayList<RemoteFile>()
|
||||||
|
|
||||||
|
// parse data from remote folder
|
||||||
|
// TODO: Remove that !!
|
||||||
|
val remoteFolder = RemoteFile.getRemoteFileFromDav(
|
||||||
|
davResource = propfindMethod.root!!,
|
||||||
|
userId = AccountUtils.getUserId(mAccount, mContext),
|
||||||
|
userName = mAccount.name
|
||||||
|
)
|
||||||
|
mFolderAndFiles.add(remoteFolder)
|
||||||
|
|
||||||
|
// loop to update every child
|
||||||
|
propfindMethod.members.forEach { resource ->
|
||||||
|
val remoteFile = RemoteFile.getRemoteFileFromDav(
|
||||||
|
davResource = resource,
|
||||||
|
userId = AccountUtils.getUserId(mAccount, mContext),
|
||||||
|
userName = mAccount.name
|
||||||
|
)
|
||||||
|
mFolderAndFiles.add(remoteFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result of the operation
|
||||||
|
return RemoteOperationResult<ArrayList<RemoteFile>>(ResultCode.OK).apply {
|
||||||
|
data = mFolderAndFiles
|
||||||
|
Timber.i("Synchronized $remotePath with ${mFolderAndFiles.size} files. ${this.logMessage}")
|
||||||
|
}
|
||||||
|
} else { // synchronization failed
|
||||||
|
return RemoteOperationResult<ArrayList<RemoteFile>>(propfindMethod).also {
|
||||||
|
Timber.w("Synchronized $remotePath ${it.logMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return RemoteOperationResult<ArrayList<RemoteFile>>(e).also {
|
||||||
|
Timber.e(it.exception, "Synchronized $remotePath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int): Boolean = status.isOneOf(HTTP_OK, HTTP_MULTI_STATUS)
|
||||||
|
}
|
@ -1,363 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
import at.bitfire.dav4jvm.Property;
|
|
||||||
import at.bitfire.dav4jvm.Response;
|
|
||||||
import at.bitfire.dav4jvm.property.CreationDate;
|
|
||||||
import at.bitfire.dav4jvm.property.GetContentLength;
|
|
||||||
import at.bitfire.dav4jvm.property.GetContentType;
|
|
||||||
import at.bitfire.dav4jvm.property.GetETag;
|
|
||||||
import at.bitfire.dav4jvm.property.GetLastModified;
|
|
||||||
import at.bitfire.dav4jvm.property.OCId;
|
|
||||||
import at.bitfire.dav4jvm.property.OCPermissions;
|
|
||||||
import at.bitfire.dav4jvm.property.OCPrivatelink;
|
|
||||||
import at.bitfire.dav4jvm.property.OCSize;
|
|
||||||
import at.bitfire.dav4jvm.property.QuotaAvailableBytes;
|
|
||||||
import at.bitfire.dav4jvm.property.QuotaUsedBytes;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.properties.OCShareTypes;
|
|
||||||
import com.owncloud.android.lib.resources.shares.ShareType;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains the data of a Remote File from a WebDavEntry
|
|
||||||
*
|
|
||||||
* @author masensio
|
|
||||||
* @author Christian Schabesberger
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class RemoteFile implements Parcelable, Serializable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parcelable Methods
|
|
||||||
*/
|
|
||||||
public static final Parcelable.Creator<RemoteFile> CREATOR = new Parcelable.Creator<RemoteFile>() {
|
|
||||||
@Override
|
|
||||||
public RemoteFile createFromParcel(Parcel source) {
|
|
||||||
return new RemoteFile(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RemoteFile[] newArray(int size) {
|
|
||||||
return new RemoteFile[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* Generated - should be refreshed every time the class changes!!
|
|
||||||
*/
|
|
||||||
private static final long serialVersionUID = -8965995357413958539L;
|
|
||||||
private String mRemotePath;
|
|
||||||
private String mMimeType;
|
|
||||||
private long mLength;
|
|
||||||
private long mCreationTimestamp;
|
|
||||||
private long mModifiedTimestamp;
|
|
||||||
private String mEtag;
|
|
||||||
private String mPermissions;
|
|
||||||
private String mRemoteId;
|
|
||||||
private long mSize;
|
|
||||||
private BigDecimal mQuotaUsedBytes;
|
|
||||||
private BigDecimal mQuotaAvailableBytes;
|
|
||||||
private String mPrivateLink;
|
|
||||||
private boolean mSharedByLink;
|
|
||||||
private boolean mSharedWithSharee;
|
|
||||||
|
|
||||||
public RemoteFile() {
|
|
||||||
resetData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new {@link RemoteFile} with given path.
|
|
||||||
* <p>
|
|
||||||
* The path received must be URL-decoded. Path separator must be File.separator, and it must be the first
|
|
||||||
* character in 'path'.
|
|
||||||
*
|
|
||||||
* @param path The remote path of the file.
|
|
||||||
*/
|
|
||||||
public RemoteFile(String path) {
|
|
||||||
resetData();
|
|
||||||
if (path == null || path.length() <= 0 || !path.startsWith(File.separator)) {
|
|
||||||
throw new IllegalArgumentException("Trying to create a OCFile with a non valid remote path: " + path);
|
|
||||||
}
|
|
||||||
mRemotePath = path;
|
|
||||||
mCreationTimestamp = 0;
|
|
||||||
mLength = 0;
|
|
||||||
mMimeType = FileUtils.MIME_DIR;
|
|
||||||
mQuotaUsedBytes = BigDecimal.ZERO;
|
|
||||||
mQuotaAvailableBytes = BigDecimal.ZERO;
|
|
||||||
mPrivateLink = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemoteFile(final Response davResource, String userId) {
|
|
||||||
this(RemoteFileUtil.Companion.getRemotePathFromUrl(davResource.getHref(), userId));
|
|
||||||
final List<Property> properties = davResource.getProperties();
|
|
||||||
|
|
||||||
for (Property property : properties) {
|
|
||||||
if (property instanceof CreationDate) {
|
|
||||||
this.setCreationTimestamp(
|
|
||||||
Long.parseLong(((CreationDate) property).getCreationDate()));
|
|
||||||
}
|
|
||||||
if (property instanceof GetContentLength) {
|
|
||||||
this.setLength(((GetContentLength) property).getContentLength());
|
|
||||||
}
|
|
||||||
if (property instanceof GetContentType) {
|
|
||||||
this.setMimeType(((GetContentType) property).getType());
|
|
||||||
}
|
|
||||||
if (property instanceof GetLastModified) {
|
|
||||||
this.setModifiedTimestamp(((GetLastModified) property).getLastModified());
|
|
||||||
}
|
|
||||||
if (property instanceof GetETag) {
|
|
||||||
this.setEtag(((GetETag) property).getETag());
|
|
||||||
}
|
|
||||||
if (property instanceof OCPermissions) {
|
|
||||||
this.setPermissions(((OCPermissions) property).getPermission());
|
|
||||||
}
|
|
||||||
if (property instanceof OCId) {
|
|
||||||
this.setRemoteId(((OCId) property).getId());
|
|
||||||
}
|
|
||||||
if (property instanceof OCSize) {
|
|
||||||
this.setSize(((OCSize) property).getSize());
|
|
||||||
}
|
|
||||||
if (property instanceof QuotaUsedBytes) {
|
|
||||||
this.setQuotaUsedBytes(
|
|
||||||
BigDecimal.valueOf(((QuotaUsedBytes) property).getQuotaUsedBytes()));
|
|
||||||
}
|
|
||||||
if (property instanceof QuotaAvailableBytes) {
|
|
||||||
this.setQuotaAvailableBytes(
|
|
||||||
BigDecimal.valueOf(((QuotaAvailableBytes) property).getQuotaAvailableBytes()));
|
|
||||||
}
|
|
||||||
if (property instanceof OCPrivatelink) {
|
|
||||||
this.setPrivateLink(((OCPrivatelink) property).getLink());
|
|
||||||
}
|
|
||||||
if (property instanceof OCShareTypes) {
|
|
||||||
LinkedList<String> list = ((OCShareTypes) property).getShareTypes();
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
ShareType shareType = ShareType.Companion.fromValue(Integer.parseInt(list.get(i)));
|
|
||||||
if (shareType == null) {
|
|
||||||
Timber.d("Illegal share type value: " + list.get(i));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (shareType.equals(ShareType.PUBLIC_LINK)) {
|
|
||||||
this.setSharedViaLink(true);
|
|
||||||
} else if (shareType.equals(ShareType.USER) ||
|
|
||||||
shareType.equals(ShareType.FEDERATED) ||
|
|
||||||
shareType.equals(ShareType.GROUP)) {
|
|
||||||
this.setSharedWithSharee(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reconstruct from parcel
|
|
||||||
*
|
|
||||||
* @param source The source parcel
|
|
||||||
*/
|
|
||||||
protected RemoteFile(Parcel source) {
|
|
||||||
readFromParcel(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use this to find out if this file is a folder.
|
|
||||||
*
|
|
||||||
* @return true if it is a folder
|
|
||||||
*/
|
|
||||||
public boolean isFolder() {
|
|
||||||
return mMimeType != null && (mMimeType.equals(FileUtils.MIME_DIR) || mMimeType.equals(FileUtils.MIME_DIR_UNIX));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Getters and Setters
|
|
||||||
*/
|
|
||||||
|
|
||||||
public String getRemotePath() {
|
|
||||||
return mRemotePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRemotePath(String remotePath) {
|
|
||||||
this.mRemotePath = remotePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMimeType() {
|
|
||||||
return mMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMimeType(String mimeType) {
|
|
||||||
this.mMimeType = mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLength() {
|
|
||||||
return mLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLength(long length) {
|
|
||||||
this.mLength = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getCreationTimestamp() {
|
|
||||||
return mCreationTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCreationTimestamp(long creationTimestamp) {
|
|
||||||
this.mCreationTimestamp = creationTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getModifiedTimestamp() {
|
|
||||||
return mModifiedTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setModifiedTimestamp(long modifiedTimestamp) {
|
|
||||||
this.mModifiedTimestamp = modifiedTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEtag() {
|
|
||||||
return mEtag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEtag(String etag) {
|
|
||||||
this.mEtag = etag;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPermissions() {
|
|
||||||
return mPermissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPermissions(String permissions) {
|
|
||||||
this.mPermissions = permissions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRemoteId() {
|
|
||||||
return mRemoteId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRemoteId(String remoteId) {
|
|
||||||
this.mRemoteId = remoteId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getSize() {
|
|
||||||
return mSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSize(long size) {
|
|
||||||
mSize = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuotaUsedBytes(BigDecimal quotaUsedBytes) {
|
|
||||||
mQuotaUsedBytes = quotaUsedBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuotaAvailableBytes(BigDecimal quotaAvailableBytes) {
|
|
||||||
mQuotaAvailableBytes = quotaAvailableBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPrivateLink() {
|
|
||||||
return mPrivateLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPrivateLink(String privateLink) {
|
|
||||||
mPrivateLink = privateLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSharedWithSharee(boolean shareWithSharee) {
|
|
||||||
mSharedWithSharee = shareWithSharee;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSharedWithSharee() {
|
|
||||||
return mSharedWithSharee;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSharedViaLink(boolean sharedViaLink) {
|
|
||||||
mSharedByLink = sharedViaLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSharedByLink() {
|
|
||||||
return mSharedByLink;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used internally. Reset all file properties
|
|
||||||
*/
|
|
||||||
private void resetData() {
|
|
||||||
mRemotePath = null;
|
|
||||||
mMimeType = null;
|
|
||||||
mLength = 0;
|
|
||||||
mCreationTimestamp = 0;
|
|
||||||
mModifiedTimestamp = 0;
|
|
||||||
mEtag = null;
|
|
||||||
mPermissions = null;
|
|
||||||
mRemoteId = null;
|
|
||||||
mSize = 0;
|
|
||||||
mQuotaUsedBytes = null;
|
|
||||||
mQuotaAvailableBytes = null;
|
|
||||||
mPrivateLink = null;
|
|
||||||
mSharedWithSharee = false;
|
|
||||||
mSharedByLink = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void readFromParcel(Parcel source) {
|
|
||||||
mRemotePath = source.readString();
|
|
||||||
mMimeType = source.readString();
|
|
||||||
mLength = source.readLong();
|
|
||||||
mCreationTimestamp = source.readLong();
|
|
||||||
mModifiedTimestamp = source.readLong();
|
|
||||||
mEtag = source.readString();
|
|
||||||
mPermissions = source.readString();
|
|
||||||
mRemoteId = source.readString();
|
|
||||||
mSize = source.readLong();
|
|
||||||
mQuotaUsedBytes = (BigDecimal) source.readSerializable();
|
|
||||||
mQuotaAvailableBytes = (BigDecimal) source.readSerializable();
|
|
||||||
mPrivateLink = source.readString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return this.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
|
||||||
dest.writeString(mRemotePath);
|
|
||||||
dest.writeString(mMimeType);
|
|
||||||
dest.writeLong(mLength);
|
|
||||||
dest.writeLong(mCreationTimestamp);
|
|
||||||
dest.writeLong(mModifiedTimestamp);
|
|
||||||
dest.writeString(mEtag);
|
|
||||||
dest.writeString(mPermissions);
|
|
||||||
dest.writeString(mRemoteId);
|
|
||||||
dest.writeLong(mSize);
|
|
||||||
dest.writeSerializable(mQuotaUsedBytes);
|
|
||||||
dest.writeSerializable(mQuotaAvailableBytes);
|
|
||||||
dest.writeString(mPrivateLink);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,189 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcelable
|
||||||
|
import at.bitfire.dav4jvm.PropStat
|
||||||
|
import at.bitfire.dav4jvm.Property
|
||||||
|
import at.bitfire.dav4jvm.Response
|
||||||
|
import at.bitfire.dav4jvm.property.CreationDate
|
||||||
|
import at.bitfire.dav4jvm.property.GetContentLength
|
||||||
|
import at.bitfire.dav4jvm.property.GetContentType
|
||||||
|
import at.bitfire.dav4jvm.property.GetETag
|
||||||
|
import at.bitfire.dav4jvm.property.GetLastModified
|
||||||
|
import at.bitfire.dav4jvm.property.OCId
|
||||||
|
import at.bitfire.dav4jvm.property.OCPermissions
|
||||||
|
import at.bitfire.dav4jvm.property.OCPrivatelink
|
||||||
|
import at.bitfire.dav4jvm.property.OCSize
|
||||||
|
import at.bitfire.dav4jvm.property.QuotaAvailableBytes
|
||||||
|
import at.bitfire.dav4jvm.property.QuotaUsedBytes
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.properties.OCShareTypes
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import com.owncloud.android.lib.resources.shares.ShareType
|
||||||
|
import com.owncloud.android.lib.resources.shares.ShareType.Companion.fromValue
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains the data of a Remote File from a WebDavEntry
|
||||||
|
*
|
||||||
|
* The path received must be URL-decoded. Path separator must be File.separator, and it must be the first character in 'path'.
|
||||||
|
*
|
||||||
|
* @author masensio
|
||||||
|
* @author Christian Schabesberger
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class RemoteFile(
|
||||||
|
var remotePath: String,
|
||||||
|
var mimeType: String = "DIR",
|
||||||
|
var length: Long = 0,
|
||||||
|
var creationTimestamp: Long = 0,
|
||||||
|
var modifiedTimestamp: Long = 0,
|
||||||
|
var etag: String? = null,
|
||||||
|
var permissions: String? = null,
|
||||||
|
var remoteId: String? = null,
|
||||||
|
var size: Long = 0,
|
||||||
|
var quotaUsedBytes: BigDecimal? = null,
|
||||||
|
var quotaAvailableBytes: BigDecimal? = null,
|
||||||
|
var privateLink: String? = null,
|
||||||
|
var owner: String,
|
||||||
|
var sharedByLink: Boolean = false,
|
||||||
|
var sharedWithSharee: Boolean = false,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
// TODO: Quotas not used. Use or remove them.
|
||||||
|
init {
|
||||||
|
require(
|
||||||
|
!(remotePath.isEmpty() || !remotePath.startsWith(File.separator))
|
||||||
|
) { "Trying to create a OCFile with a non valid remote path: $remotePath" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to find out if this file is a folder.
|
||||||
|
*
|
||||||
|
* @return true if it is a folder
|
||||||
|
*/
|
||||||
|
val isFolder
|
||||||
|
get() = mimeType.isOneOf(MIME_DIR, MIME_DIR_UNIX)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val MIME_DIR = "DIR"
|
||||||
|
const val MIME_DIR_UNIX = "httpd/unix-directory"
|
||||||
|
|
||||||
|
fun getRemoteFileFromDav(davResource: Response, userId: String, userName: String): RemoteFile {
|
||||||
|
val remotePath = getRemotePathFromUrl(davResource.href, userId)
|
||||||
|
val remoteFile = RemoteFile(remotePath = remotePath, owner = userName)
|
||||||
|
val properties = getPropertiesEvenIfPostProcessing(davResource)
|
||||||
|
|
||||||
|
for (property in properties) {
|
||||||
|
when (property) {
|
||||||
|
is CreationDate -> {
|
||||||
|
remoteFile.creationTimestamp = property.creationDate.toLong()
|
||||||
|
}
|
||||||
|
is GetContentLength -> {
|
||||||
|
remoteFile.length = property.contentLength
|
||||||
|
}
|
||||||
|
is GetContentType -> {
|
||||||
|
property.type?.let { remoteFile.mimeType = it }
|
||||||
|
}
|
||||||
|
is GetLastModified -> {
|
||||||
|
remoteFile.modifiedTimestamp = property.lastModified
|
||||||
|
}
|
||||||
|
is GetETag -> {
|
||||||
|
remoteFile.etag = property.eTag
|
||||||
|
}
|
||||||
|
is OCPermissions -> {
|
||||||
|
remoteFile.permissions = property.permission
|
||||||
|
}
|
||||||
|
is OCId -> {
|
||||||
|
remoteFile.remoteId = property.id
|
||||||
|
}
|
||||||
|
is OCSize -> {
|
||||||
|
remoteFile.size = property.size
|
||||||
|
}
|
||||||
|
is QuotaUsedBytes -> {
|
||||||
|
remoteFile.quotaUsedBytes = BigDecimal.valueOf(property.quotaUsedBytes)
|
||||||
|
}
|
||||||
|
is QuotaAvailableBytes -> {
|
||||||
|
remoteFile.quotaAvailableBytes = BigDecimal.valueOf(property.quotaAvailableBytes)
|
||||||
|
}
|
||||||
|
is OCPrivatelink -> {
|
||||||
|
remoteFile.privateLink = property.link
|
||||||
|
}
|
||||||
|
is OCShareTypes -> {
|
||||||
|
val list = property.shareTypes
|
||||||
|
for (i in list.indices) {
|
||||||
|
val shareType = fromValue(list[i].toInt())
|
||||||
|
if (shareType == null) {
|
||||||
|
Timber.d("Illegal share type value: " + list[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (shareType == ShareType.PUBLIC_LINK) {
|
||||||
|
remoteFile.sharedByLink = true
|
||||||
|
} else if (shareType == ShareType.USER || shareType == ShareType.FEDERATED || shareType == ShareType.GROUP) {
|
||||||
|
remoteFile.sharedWithSharee = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remoteFile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a relative path from a remote file url
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Example: url:port/remote.php/dav/files/username/Documents/text.txt => /Documents/text.txt
|
||||||
|
*
|
||||||
|
* @param url remote file url
|
||||||
|
* @param userId file owner
|
||||||
|
* @return remote relative path of the file
|
||||||
|
*/
|
||||||
|
private fun getRemotePathFromUrl(url: HttpUrl, userId: String): String {
|
||||||
|
val davFilesPath = OwnCloudClient.WEBDAV_FILES_PATH_4_0 + userId
|
||||||
|
val absoluteDavPath = Uri.decode(url.encodedPath)
|
||||||
|
val pathToOc = absoluteDavPath.split(davFilesPath).first()
|
||||||
|
return absoluteDavPath.replace(pathToOc + davFilesPath, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPropertiesEvenIfPostProcessing(response: Response): List<Property> {
|
||||||
|
return if (response.isSuccess())
|
||||||
|
response.propstat.filter { propStat -> propStat.isSuccessOrPostProcessing() }.map { it.properties }.flatten()
|
||||||
|
else
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PropStat.isSuccessOrPostProcessing() = (status.code / 100 == 2 || status.code == HttpConstants.HTTP_TOO_EARLY)
|
||||||
|
}
|
||||||
|
}
|
@ -1,96 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.nonwebdav.DeleteMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the removal of a remote file or folder in the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
public class RemoveRemoteFileOperation extends RemoteOperation {
|
|
||||||
private String mRemotePath;
|
|
||||||
|
|
||||||
protected boolean removeChunksFolder = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param remotePath RemotePath of the remote file or folder to remove from the server
|
|
||||||
*/
|
|
||||||
public RemoveRemoteFileOperation(String remotePath) {
|
|
||||||
mRemotePath = remotePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the rename operation.
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult run(OwnCloudClient client) {
|
|
||||||
RemoteOperationResult result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Uri srcWebDavUri = removeChunksFolder ? client.getUploadsWebDavUri() : client.getUserFilesWebDavUri();
|
|
||||||
|
|
||||||
DeleteMethod deleteMethod = new DeleteMethod(
|
|
||||||
new URL(srcWebDavUri + WebdavUtils.encodePath(mRemotePath)));
|
|
||||||
|
|
||||||
int status = client.executeHttpMethod(deleteMethod);
|
|
||||||
|
|
||||||
result = isSuccess(status) ?
|
|
||||||
new RemoteOperationResult<>(OK) :
|
|
||||||
new RemoteOperationResult<>(deleteMethod);
|
|
||||||
|
|
||||||
Timber.i("Remove " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Remove " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSuccess(int status) {
|
|
||||||
return status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_NO_CONTENT;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,81 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants.HTTP_NO_CONTENT
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants.HTTP_OK
|
||||||
|
import com.owncloud.android.lib.common.http.methods.nonwebdav.DeleteMethod
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the removal of a remote file or folder in the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*/
|
||||||
|
open class RemoveRemoteFileOperation(
|
||||||
|
private val remotePath: String
|
||||||
|
) : RemoteOperation<Unit>() {
|
||||||
|
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
var result: RemoteOperationResult<Unit>
|
||||||
|
try {
|
||||||
|
val srcWebDavUri = getSrcWebDavUriForClient(client)
|
||||||
|
val deleteMethod = DeleteMethod(
|
||||||
|
URL(srcWebDavUri.toString() + WebdavUtils.encodePath(remotePath))
|
||||||
|
)
|
||||||
|
val status = client.executeHttpMethod(deleteMethod)
|
||||||
|
|
||||||
|
result = if (isSuccess(status)) {
|
||||||
|
RemoteOperationResult<Unit>(ResultCode.OK)
|
||||||
|
} else {
|
||||||
|
RemoteOperationResult<Unit>(deleteMethod)
|
||||||
|
}
|
||||||
|
Timber.i("Remove $remotePath: ${result.logMessage}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result = RemoteOperationResult<Unit>(e)
|
||||||
|
Timber.e(e, "Remove $remotePath: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For standard removals, we will use [OwnCloudClient.getUserFilesWebDavUri].
|
||||||
|
* In case we need a different source Uri, override this method.
|
||||||
|
*/
|
||||||
|
open fun getSrcWebDavUriForClient(client: OwnCloudClient): Uri = client.userFilesWebDavUri
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int) = status.isOneOf(HTTP_OK, HTTP_NO_CONTENT)
|
||||||
|
}
|
@ -1,131 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2019 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.MoveMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the rename of a remote file or folder in the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
*/
|
|
||||||
public class RenameRemoteFileOperation extends RemoteOperation {
|
|
||||||
|
|
||||||
private static final int RENAME_READ_TIMEOUT = 600000;
|
|
||||||
private static final int RENAME_CONNECTION_TIMEOUT = 5000;
|
|
||||||
|
|
||||||
private String mOldName;
|
|
||||||
private String mOldRemotePath;
|
|
||||||
private String mNewName;
|
|
||||||
private String mNewRemotePath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param oldName Old name of the file.
|
|
||||||
* @param oldRemotePath Old remote path of the file.
|
|
||||||
* @param newName New name to set as the name of file.
|
|
||||||
* @param isFolder 'true' for folder and 'false' for files
|
|
||||||
*/
|
|
||||||
public RenameRemoteFileOperation(String oldName, String oldRemotePath, String newName,
|
|
||||||
boolean isFolder) {
|
|
||||||
mOldName = oldName;
|
|
||||||
mOldRemotePath = oldRemotePath;
|
|
||||||
mNewName = newName;
|
|
||||||
|
|
||||||
String parent = (new File(mOldRemotePath)).getParent();
|
|
||||||
parent = (parent.endsWith(File.separator)) ? parent : parent + File.separator;
|
|
||||||
mNewRemotePath = parent + mNewName;
|
|
||||||
if (isFolder) {
|
|
||||||
mNewRemotePath += File.separator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs the rename operation.
|
|
||||||
*
|
|
||||||
* @param client Client object to communicate with the remote ownCloud server.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult run(OwnCloudClient client) {
|
|
||||||
try {
|
|
||||||
if (mNewName.equals(mOldName)) {
|
|
||||||
return new RemoteOperationResult<>(ResultCode.OK);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetPathIsUsed(client)) {
|
|
||||||
return new RemoteOperationResult<>(ResultCode.INVALID_OVERWRITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
final MoveMethod move = new MoveMethod(
|
|
||||||
new URL(client.getUserFilesWebDavUri() +
|
|
||||||
WebdavUtils.encodePath(mOldRemotePath)),
|
|
||||||
client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mNewRemotePath), false);
|
|
||||||
|
|
||||||
move.setReadTimeout(RENAME_READ_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
move.setConnectionTimeout(RENAME_READ_TIMEOUT, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
final int status = client.executeHttpMethod(move);
|
|
||||||
final RemoteOperationResult result =
|
|
||||||
(status == HttpConstants.HTTP_CREATED || status == HttpConstants.HTTP_NO_CONTENT)
|
|
||||||
? new RemoteOperationResult<>(ResultCode.OK)
|
|
||||||
: new RemoteOperationResult<>(move);
|
|
||||||
|
|
||||||
Timber.i("Rename " + mOldRemotePath + " to " + mNewRemotePath + ": " + result.getLogMessage());
|
|
||||||
client.exhaustResponse(move.getResponseBodyAsStream());
|
|
||||||
return result;
|
|
||||||
} catch (Exception e) {
|
|
||||||
final RemoteOperationResult result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e,
|
|
||||||
"Rename " + mOldRemotePath + " to " + ((mNewRemotePath == null) ? mNewName : mNewRemotePath) + ":" +
|
|
||||||
" " + result.getLogMessage());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a file with the new name already exists.
|
|
||||||
*
|
|
||||||
* @return 'True' if the target path is already used by an existing file.
|
|
||||||
*/
|
|
||||||
private boolean targetPathIsUsed(OwnCloudClient client) {
|
|
||||||
CheckPathExistenceRemoteOperation checkPathExistenceRemoteOperation =
|
|
||||||
new CheckPathExistenceRemoteOperation(mNewRemotePath, false);
|
|
||||||
RemoteOperationResult exists = checkPathExistenceRemoteOperation.execute(client);
|
|
||||||
return exists.isSuccess();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,119 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.MoveMethod
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the rename of a remote file or folder in the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
*/
|
||||||
|
class RenameRemoteFileOperation(
|
||||||
|
private val oldName: String,
|
||||||
|
private val oldRemotePath: String,
|
||||||
|
private val newName: String,
|
||||||
|
isFolder: Boolean,
|
||||||
|
) : RemoteOperation<Unit>() {
|
||||||
|
|
||||||
|
private var newRemotePath: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
var parent = (File(oldRemotePath)).parent ?: throw IllegalArgumentException()
|
||||||
|
if (!parent.endsWith(File.separator)) {
|
||||||
|
parent = parent.plus(File.separator)
|
||||||
|
}
|
||||||
|
newRemotePath = parent.plus(newName)
|
||||||
|
if (isFolder) {
|
||||||
|
newRemotePath.plus(File.separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
var result: RemoteOperationResult<Unit>
|
||||||
|
try {
|
||||||
|
if (newName == oldName) {
|
||||||
|
return RemoteOperationResult<Unit>(ResultCode.OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetPathIsUsed(client)) {
|
||||||
|
return RemoteOperationResult<Unit>(ResultCode.INVALID_OVERWRITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
val moveMethod: MoveMethod = MoveMethod(
|
||||||
|
url = URL(client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(oldRemotePath)),
|
||||||
|
destinationUrl = client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(newRemotePath),
|
||||||
|
).apply {
|
||||||
|
setReadTimeout(RENAME_READ_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
|
setConnectionTimeout(RENAME_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
val status = client.executeHttpMethod(moveMethod)
|
||||||
|
|
||||||
|
result = if (isSuccess(status)) {
|
||||||
|
RemoteOperationResult<Unit>(ResultCode.OK)
|
||||||
|
} else {
|
||||||
|
RemoteOperationResult<Unit>(moveMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.i("Rename $oldRemotePath to $newRemotePath: ${result.logMessage}")
|
||||||
|
client.exhaustResponse(moveMethod.getResponseBodyAsStream())
|
||||||
|
return result
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
result = RemoteOperationResult<Unit>(exception)
|
||||||
|
Timber.e(exception, "Rename $oldRemotePath to $newName: ${result.logMessage}")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a file with the new name already exists.
|
||||||
|
*
|
||||||
|
* @return 'True' if the target path is already used by an existing file.
|
||||||
|
*/
|
||||||
|
private fun targetPathIsUsed(client: OwnCloudClient): Boolean {
|
||||||
|
val checkPathExistenceRemoteOperation = CheckPathExistenceRemoteOperation(newRemotePath, false)
|
||||||
|
val exists = checkPathExistenceRemoteOperation.execute(client)
|
||||||
|
return exists.isSuccess
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int) = status.isOneOf(HttpConstants.HTTP_CREATED, HttpConstants.HTTP_NO_CONTENT)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RENAME_READ_TIMEOUT = 10_000L
|
||||||
|
private const val RENAME_CONNECTION_TIMEOUT = 5_000L
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
/* ownCloud Android Library is available under MIT license
|
||||||
* Copyright (C) 2021 ownCloud GmbH.
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -24,22 +24,15 @@
|
|||||||
|
|
||||||
package com.owncloud.android.lib.resources.files
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.PutMethod
|
import com.owncloud.android.lib.common.http.methods.webdav.PutMethod
|
||||||
|
import com.owncloud.android.lib.common.network.ContentUriRequestBody
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
import okhttp3.MediaType
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okio.BufferedSink
|
|
||||||
import okio.source
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class UploadFileFromContentUriOperation(
|
class UploadFileFromContentUriOperation(
|
||||||
@ -70,34 +63,6 @@ class UploadFileFromContentUriOperation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isSuccess(status: Int): Boolean {
|
fun isSuccess(status: Int): Boolean {
|
||||||
return status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_CREATED || status == HttpConstants.HTTP_NO_CONTENT
|
return status.isOneOf(HttpConstants.HTTP_OK, HttpConstants.HTTP_CREATED, HttpConstants.HTTP_NO_CONTENT)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContentUriRequestBody(
|
|
||||||
private val contentResolver: ContentResolver,
|
|
||||||
private val contentUri: Uri
|
|
||||||
) : RequestBody() {
|
|
||||||
|
|
||||||
override fun contentType(): MediaType? {
|
|
||||||
val contentType = contentResolver.getType(contentUri) ?: return null
|
|
||||||
return contentType.toMediaTypeOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun contentLength(): Long {
|
|
||||||
contentResolver.query(contentUri, null, null, null, null)?.use { cursor ->
|
|
||||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
|
||||||
cursor.moveToFirst()
|
|
||||||
return cursor.getLong(sizeIndex)
|
|
||||||
} ?: return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun writeTo(sink: BufferedSink) {
|
|
||||||
val inputStream = contentResolver.openInputStream(contentUri)
|
|
||||||
?: throw IOException("Couldn't open content URI for reading: $contentUri")
|
|
||||||
|
|
||||||
inputStream.source().use { source ->
|
|
||||||
sink.writeAll(source)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.PutMethod
|
||||||
|
import com.owncloud.android.lib.common.network.FileRequestBody
|
||||||
|
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
|
||||||
|
import com.owncloud.android.lib.common.network.WebdavUtils
|
||||||
|
import com.owncloud.android.lib.common.operations.OperationCancelledException
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.common.utils.isOneOf
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the upload of a remote file to the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author masensio
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*/
|
||||||
|
open class UploadFileFromFileSystemOperation(
|
||||||
|
val localPath: String,
|
||||||
|
val remotePath: String,
|
||||||
|
val mimeType: String,
|
||||||
|
val lastModifiedTimestamp: String,
|
||||||
|
val requiredEtag: String?,
|
||||||
|
) : RemoteOperation<Unit>() {
|
||||||
|
|
||||||
|
protected val cancellationRequested = AtomicBoolean(false)
|
||||||
|
protected var putMethod: PutMethod? = null
|
||||||
|
protected val dataTransferListener: MutableSet<OnDatatransferProgressListener> = HashSet()
|
||||||
|
protected var fileRequestBody: FileRequestBody? = null
|
||||||
|
|
||||||
|
var etag: String = ""
|
||||||
|
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
var result: RemoteOperationResult<Unit>
|
||||||
|
try {
|
||||||
|
if (cancellationRequested.get()) {
|
||||||
|
// the operation was cancelled before getting it's turn to be executed in the queue of uploads
|
||||||
|
result = RemoteOperationResult<Unit>(OperationCancelledException())
|
||||||
|
Timber.i("Upload of $localPath to $remotePath has been cancelled")
|
||||||
|
} else {
|
||||||
|
// perform the upload
|
||||||
|
result = uploadFile(client)
|
||||||
|
Timber.i("Upload of $localPath to $remotePath: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (putMethod?.isAborted == true) {
|
||||||
|
result = RemoteOperationResult<Unit>(OperationCancelledException())
|
||||||
|
Timber.e(result.exception, "Upload of $localPath to $remotePath has been aborted with this message: ${result.logMessage}")
|
||||||
|
} else {
|
||||||
|
result = RemoteOperationResult<Unit>(e)
|
||||||
|
Timber.e(result.exception, "Upload of $localPath to $remotePath has failed with this message: ${result.logMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
protected open fun uploadFile(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
val fileToUpload = File(localPath)
|
||||||
|
val mediaType: MediaType? = mimeType.toMediaTypeOrNull()
|
||||||
|
|
||||||
|
fileRequestBody = FileRequestBody(fileToUpload, mediaType).also {
|
||||||
|
synchronized(dataTransferListener) { it.addDatatransferProgressListeners(dataTransferListener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
putMethod = PutMethod(URL(client.userFilesWebDavUri.toString() + WebdavUtils.encodePath(remotePath)), fileRequestBody!!).apply {
|
||||||
|
retryOnConnectionFailure = false
|
||||||
|
if (!requiredEtag.isNullOrBlank()) {
|
||||||
|
addRequestHeader(HttpConstants.IF_MATCH_HEADER, requiredEtag)
|
||||||
|
}
|
||||||
|
addRequestHeader(HttpConstants.OC_TOTAL_LENGTH_HEADER, fileToUpload.length().toString())
|
||||||
|
addRequestHeader(HttpConstants.OC_X_OC_MTIME_HEADER, lastModifiedTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = client.executeHttpMethod(putMethod)
|
||||||
|
return if (isSuccess(status)) {
|
||||||
|
etag = WebdavUtils.getEtagFromResponse(putMethod)
|
||||||
|
// Get rid of extra quotas
|
||||||
|
etag = etag.replace("\"", "")
|
||||||
|
if (etag.isEmpty()) {
|
||||||
|
Timber.e("Could not read eTag from response uploading %s", localPath)
|
||||||
|
} else {
|
||||||
|
Timber.d("File uploaded successfully. New etag for file ${fileToUpload.name} is $etag")
|
||||||
|
}
|
||||||
|
RemoteOperationResult<Unit>(ResultCode.OK).apply { data = Unit }
|
||||||
|
} else { // synchronization failed
|
||||||
|
RemoteOperationResult<Unit>(putMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addDataTransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListener) { dataTransferListener.add(listener) }
|
||||||
|
fileRequestBody?.addDatatransferProgressListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeDataTransferProgressListener(listener: OnDatatransferProgressListener) {
|
||||||
|
synchronized(dataTransferListener) { dataTransferListener.remove(listener) }
|
||||||
|
fileRequestBody?.removeDatatransferProgressListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
synchronized(cancellationRequested) {
|
||||||
|
cancellationRequested.set(true)
|
||||||
|
putMethod?.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSuccess(status: Int): Boolean {
|
||||||
|
return status.isOneOf(HttpConstants.HTTP_OK, HttpConstants.HTTP_CREATED, HttpConstants.HTTP_NO_CONTENT)
|
||||||
|
}
|
||||||
|
}
|
@ -1,181 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.HttpConstants;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.PutMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.FileRequestBody;
|
|
||||||
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
|
|
||||||
import com.owncloud.android.lib.common.network.WebdavUtils;
|
|
||||||
import com.owncloud.android.lib.common.operations.OperationCancelledException;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperation;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the upload of a remote file to the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author masensio
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class UploadRemoteFileOperation extends RemoteOperation {
|
|
||||||
|
|
||||||
protected final AtomicBoolean mCancellationRequested = new AtomicBoolean(false);
|
|
||||||
protected String mLocalPath;
|
|
||||||
protected String mRemotePath;
|
|
||||||
protected String mMimeType;
|
|
||||||
protected String mFileLastModifTimestamp;
|
|
||||||
protected PutMethod mPutMethod = null;
|
|
||||||
protected String mRequiredEtag = null;
|
|
||||||
protected Set<OnDatatransferProgressListener> mDataTransferListeners = new HashSet<>();
|
|
||||||
|
|
||||||
protected FileRequestBody mFileRequestBody = null;
|
|
||||||
|
|
||||||
public UploadRemoteFileOperation(String localPath, String remotePath, String mimeType,
|
|
||||||
String fileLastModifTimestamp) {
|
|
||||||
mLocalPath = localPath;
|
|
||||||
mRemotePath = remotePath;
|
|
||||||
mMimeType = mimeType;
|
|
||||||
mFileLastModifTimestamp = fileLastModifTimestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public UploadRemoteFileOperation(String localPath, String remotePath, String mimeType,
|
|
||||||
String requiredEtag, String fileLastModifTimestamp) {
|
|
||||||
this(localPath, remotePath, mimeType, fileLastModifTimestamp);
|
|
||||||
mRequiredEtag = requiredEtag;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult run(OwnCloudClient client) {
|
|
||||||
RemoteOperationResult result;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
if (mCancellationRequested.get()) {
|
|
||||||
// the operation was cancelled before getting it's turn to be executed in the queue of uploads
|
|
||||||
result = new RemoteOperationResult<>(new OperationCancelledException());
|
|
||||||
} else {
|
|
||||||
// perform the upload
|
|
||||||
result = uploadFile(client);
|
|
||||||
Timber.i("Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
if (mPutMethod != null && mPutMethod.isAborted()) {
|
|
||||||
result = new RemoteOperationResult<>(new OperationCancelledException());
|
|
||||||
Timber.e(result.getException(),
|
|
||||||
"Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
} else {
|
|
||||||
result = new RemoteOperationResult<>(e);
|
|
||||||
Timber.e(e, "Upload of " + mLocalPath + " to " + mRemotePath + ": " + result.getLogMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected RemoteOperationResult<?> uploadFile(OwnCloudClient client) throws Exception {
|
|
||||||
|
|
||||||
File fileToUpload = new File(mLocalPath);
|
|
||||||
|
|
||||||
MediaType mediaType = MediaType.parse(mMimeType);
|
|
||||||
|
|
||||||
mFileRequestBody = new FileRequestBody(fileToUpload, mediaType);
|
|
||||||
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mFileRequestBody.addDatatransferProgressListeners(mDataTransferListeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
mPutMethod = new PutMethod(
|
|
||||||
new URL(client.getUserFilesWebDavUri() + WebdavUtils.encodePath(mRemotePath)), mFileRequestBody);
|
|
||||||
|
|
||||||
mPutMethod.setRetryOnConnectionFailure(false);
|
|
||||||
|
|
||||||
if (mRequiredEtag != null && mRequiredEtag.length() > 0) {
|
|
||||||
mPutMethod.addRequestHeader(HttpConstants.IF_MATCH_HEADER, mRequiredEtag);
|
|
||||||
}
|
|
||||||
|
|
||||||
mPutMethod.addRequestHeader(HttpConstants.OC_TOTAL_LENGTH_HEADER, String.valueOf(fileToUpload.length()));
|
|
||||||
mPutMethod.addRequestHeader(HttpConstants.OC_X_OC_MTIME_HEADER, mFileLastModifTimestamp);
|
|
||||||
|
|
||||||
int status = client.executeHttpMethod(mPutMethod);
|
|
||||||
|
|
||||||
if (isSuccess(status)) {
|
|
||||||
return new RemoteOperationResult<>(OK);
|
|
||||||
|
|
||||||
} else { // synchronization failed
|
|
||||||
return new RemoteOperationResult<>(mPutMethod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Set<OnDatatransferProgressListener> getDataTransferListeners() {
|
|
||||||
return mDataTransferListeners;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.add(listener);
|
|
||||||
}
|
|
||||||
if (mFileRequestBody != null) {
|
|
||||||
mFileRequestBody.addDatatransferProgressListener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeDatatransferProgressListener(OnDatatransferProgressListener listener) {
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mDataTransferListeners.remove(listener);
|
|
||||||
}
|
|
||||||
if (mFileRequestBody != null) {
|
|
||||||
mFileRequestBody.removeDatatransferProgressListener(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancel() {
|
|
||||||
synchronized (mCancellationRequested) {
|
|
||||||
mCancellationRequested.set(true);
|
|
||||||
if (mPutMethod != null) {
|
|
||||||
mPutMethod.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSuccess(int status) {
|
|
||||||
return ((status == HttpConstants.HTTP_OK || status == HttpConstants.HTTP_CREATED ||
|
|
||||||
status == HttpConstants.HTTP_NO_CONTENT));
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,122 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files.chunks
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.PutMethod
|
||||||
|
import com.owncloud.android.lib.common.network.ChunkFromFileRequestBody
|
||||||
|
import com.owncloud.android.lib.common.operations.OperationCancelledException
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||||
|
import com.owncloud.android.lib.resources.files.FileUtils.MODE_READ_ONLY
|
||||||
|
import com.owncloud.android.lib.resources.files.UploadFileFromFileSystemOperation
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.io.File
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import java.net.URL
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote operation performing the chunked upload of a remote file to the ownCloud server.
|
||||||
|
*
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
|
*/
|
||||||
|
class ChunkedUploadFromFileSystemOperation(
|
||||||
|
private val transferId: String,
|
||||||
|
localPath: String,
|
||||||
|
remotePath: String,
|
||||||
|
mimeType: String,
|
||||||
|
lastModifiedTimestamp: String,
|
||||||
|
requiredEtag: String?,
|
||||||
|
) : UploadFileFromFileSystemOperation(
|
||||||
|
localPath = localPath,
|
||||||
|
remotePath = remotePath,
|
||||||
|
mimeType = mimeType,
|
||||||
|
lastModifiedTimestamp = lastModifiedTimestamp,
|
||||||
|
requiredEtag = requiredEtag
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun uploadFile(client: OwnCloudClient): RemoteOperationResult<Unit> {
|
||||||
|
lateinit var result: RemoteOperationResult<Unit>
|
||||||
|
|
||||||
|
val fileToUpload = File(localPath)
|
||||||
|
val mediaType: MediaType? = mimeType.toMediaTypeOrNull()
|
||||||
|
val raf = RandomAccessFile(fileToUpload, MODE_READ_ONLY)
|
||||||
|
val channel: FileChannel = raf.channel
|
||||||
|
|
||||||
|
val fileRequestBody = ChunkFromFileRequestBody(fileToUpload, mediaType, channel).also {
|
||||||
|
synchronized(dataTransferListener) { it.addDatatransferProgressListeners(dataTransferListener) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val uriPrefix = client.uploadsWebDavUri.toString() + File.separator + transferId
|
||||||
|
val totalLength = fileToUpload.length()
|
||||||
|
val chunkCount = ceil(totalLength.toDouble() / CHUNK_SIZE).toLong()
|
||||||
|
var offset: Long = 0
|
||||||
|
|
||||||
|
for (chunkIndex in 0..chunkCount) {
|
||||||
|
fileRequestBody.setOffset(offset)
|
||||||
|
|
||||||
|
if (cancellationRequested.get()) {
|
||||||
|
result = RemoteOperationResult<Unit>(OperationCancelledException())
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
putMethod = PutMethod(URL(uriPrefix + File.separator + chunkIndex), fileRequestBody).apply {
|
||||||
|
if (chunkIndex == chunkCount - 1) {
|
||||||
|
// Added a high timeout to the last chunk due to when the last chunk
|
||||||
|
// arrives to the server with the last PUT, all chunks get assembled
|
||||||
|
// within that PHP request, so last one takes longer.
|
||||||
|
setReadTimeout(LAST_CHUNK_TIMEOUT.toLong(), TimeUnit.MILLISECONDS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val status = client.executeHttpMethod(putMethod)
|
||||||
|
|
||||||
|
Timber.d("Upload of $localPath to $remotePath, chunk index $chunkIndex, count $chunkCount, HTTP result status $status")
|
||||||
|
|
||||||
|
if (isSuccess(status)) {
|
||||||
|
result = RemoteOperationResult<Unit>(ResultCode.OK)
|
||||||
|
} else {
|
||||||
|
result = RemoteOperationResult<Unit>(putMethod)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += CHUNK_SIZE
|
||||||
|
}
|
||||||
|
channel.close()
|
||||||
|
raf.close()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHUNK_SIZE = 10_240_000L // 10 MB
|
||||||
|
private const val LAST_CHUNK_TIMEOUT = 900_000 // 15 mins.
|
||||||
|
}
|
||||||
|
}
|
@ -1,130 +0,0 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
||||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
||||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files.chunks;
|
|
||||||
|
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient;
|
|
||||||
import com.owncloud.android.lib.common.http.methods.webdav.PutMethod;
|
|
||||||
import com.owncloud.android.lib.common.network.ChunkFromFileRequestBody;
|
|
||||||
import com.owncloud.android.lib.common.operations.OperationCancelledException;
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
|
|
||||||
import com.owncloud.android.lib.resources.files.UploadRemoteFileOperation;
|
|
||||||
import okhttp3.MediaType;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remote operation performing the chunked upload of a remote file to the ownCloud server.
|
|
||||||
*
|
|
||||||
* @author David A. Velasco
|
|
||||||
* @author David González Verdugo
|
|
||||||
*/
|
|
||||||
public class ChunkedUploadRemoteFileOperation extends UploadRemoteFileOperation {
|
|
||||||
|
|
||||||
public static final long CHUNK_SIZE = 1024000;
|
|
||||||
private static final int LAST_CHUNK_TIMEOUT = 900000; //15 mins.
|
|
||||||
|
|
||||||
private String mTransferId;
|
|
||||||
|
|
||||||
public ChunkedUploadRemoteFileOperation(String transferId, String localPath, String remotePath, String mimeType,
|
|
||||||
String requiredEtag, String fileLastModifTimestamp) {
|
|
||||||
super(localPath, remotePath, mimeType, requiredEtag, fileLastModifTimestamp);
|
|
||||||
mTransferId = transferId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected RemoteOperationResult uploadFile(OwnCloudClient client) throws Exception {
|
|
||||||
int status;
|
|
||||||
RemoteOperationResult result = null;
|
|
||||||
FileChannel channel;
|
|
||||||
RandomAccessFile raf;
|
|
||||||
|
|
||||||
File fileToUpload = new File(mLocalPath);
|
|
||||||
MediaType mediaType = MediaType.parse(mMimeType);
|
|
||||||
|
|
||||||
raf = new RandomAccessFile(fileToUpload, "r");
|
|
||||||
channel = raf.getChannel();
|
|
||||||
|
|
||||||
mFileRequestBody = new ChunkFromFileRequestBody(fileToUpload, mediaType, channel, CHUNK_SIZE);
|
|
||||||
|
|
||||||
synchronized (mDataTransferListeners) {
|
|
||||||
mFileRequestBody.addDatatransferProgressListeners(mDataTransferListeners);
|
|
||||||
}
|
|
||||||
|
|
||||||
long offset = 0;
|
|
||||||
String uriPrefix = client.getUploadsWebDavUri() + File.separator + mTransferId;
|
|
||||||
long totalLength = fileToUpload.length();
|
|
||||||
long chunkCount = (long) Math.ceil((double) totalLength / CHUNK_SIZE);
|
|
||||||
|
|
||||||
for (int chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++, offset += CHUNK_SIZE) {
|
|
||||||
|
|
||||||
((ChunkFromFileRequestBody) mFileRequestBody).setOffset(offset);
|
|
||||||
|
|
||||||
if (mCancellationRequested.get()) {
|
|
||||||
result = new RemoteOperationResult<>(new OperationCancelledException());
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
mPutMethod = new PutMethod(
|
|
||||||
new URL(uriPrefix + File.separator + chunkIndex), mFileRequestBody);
|
|
||||||
|
|
||||||
if (chunkIndex == chunkCount - 1) {
|
|
||||||
// Added a high timeout to the last chunk due to when the last chunk
|
|
||||||
// arrives to the server with the last PUT, all chunks get assembled
|
|
||||||
// within that PHP request, so last one takes longer.
|
|
||||||
mPutMethod.setReadTimeout(LAST_CHUNK_TIMEOUT, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
status = client.executeHttpMethod(mPutMethod);
|
|
||||||
|
|
||||||
Timber.d("Upload of " + mLocalPath + " to " + mRemotePath +
|
|
||||||
", chunk index " + chunkIndex + ", count " + chunkCount +
|
|
||||||
", HTTP result status " + status);
|
|
||||||
|
|
||||||
if (isSuccess(status)) {
|
|
||||||
result = new RemoteOperationResult<>(OK);
|
|
||||||
} else {
|
|
||||||
result = new RemoteOperationResult<>(mPutMethod);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channel != null) {
|
|
||||||
channel.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raf != null) {
|
|
||||||
raf.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
/* ownCloud Android Library is available under MIT license
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -21,30 +21,38 @@
|
|||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files.chunks
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files.chunks;
|
import android.net.Uri
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
import com.owncloud.android.lib.resources.files.MoveRemoteFileOperation;
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.webdav.MoveMethod
|
||||||
|
import com.owncloud.android.lib.resources.files.MoveRemoteFileOperation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remote operation to move the file built from chunks after uploading it
|
* Remote operation to move the file built from chunks after uploading it
|
||||||
*
|
*
|
||||||
* @author David González Verdugo
|
* @author David González Verdugo
|
||||||
|
* @author Abel García de Prada
|
||||||
*/
|
*/
|
||||||
public class MoveRemoteChunksFileOperation extends MoveRemoteFileOperation {
|
class MoveRemoteChunksFileOperation(
|
||||||
|
sourceRemotePath: String,
|
||||||
|
targetRemotePath: String,
|
||||||
|
private val fileLastModificationTimestamp: String,
|
||||||
|
private val fileLength: Long
|
||||||
|
) : MoveRemoteFileOperation(
|
||||||
|
sourceRemotePath = sourceRemotePath,
|
||||||
|
targetRemotePath = targetRemotePath,
|
||||||
|
) {
|
||||||
|
|
||||||
/**
|
override fun getSrcWebDavUriForClient(client: OwnCloudClient): Uri = client.uploadsWebDavUri
|
||||||
* Constructor.
|
|
||||||
*
|
override fun addRequestHeaders(moveMethod: MoveMethod) {
|
||||||
* @param srcRemotePath Remote path of the file/folder to move.
|
super.addRequestHeaders(moveMethod)
|
||||||
* @param targetRemotePath Remove path desired for the file/folder after moving it.
|
|
||||||
* @param overwrite
|
moveMethod.apply {
|
||||||
*/
|
addRequestHeader(HttpConstants.OC_X_OC_MTIME_HEADER, fileLastModificationTimestamp)
|
||||||
public MoveRemoteChunksFileOperation(String srcRemotePath, String targetRemotePath, boolean overwrite,
|
addRequestHeader(HttpConstants.OC_TOTAL_LENGTH_HEADER, fileLength.toString())
|
||||||
String fileLastModifTimestamp, long fileLength) {
|
}
|
||||||
super(srcRemotePath, targetRemotePath, overwrite);
|
|
||||||
moveChunkedFile = true;
|
|
||||||
mFileLastModifTimestamp = fileLastModifTimestamp;
|
|
||||||
mFileLength = fileLength;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -22,20 +22,12 @@
|
|||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.files.chunks
|
||||||
|
|
||||||
package com.owncloud.android.lib.resources.files.chunks;
|
import android.net.Uri
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation
|
||||||
|
|
||||||
import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation;
|
class RemoveRemoteChunksFolderOperation(remotePath: String) : RemoveRemoteFileOperation(remotePath) {
|
||||||
|
override fun getSrcWebDavUriForClient(client: OwnCloudClient): Uri = client.uploadsWebDavUri
|
||||||
public class RemoveRemoteChunksFolderOperation extends RemoveRemoteFileOperation {
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param remotePath RemotePath of the remote file or folder to remove from the server
|
|
||||||
*/
|
|
||||||
public RemoveRemoteChunksFolderOperation(String remotePath) {
|
|
||||||
super(remotePath);
|
|
||||||
removeChunksFolder = true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
/* ownCloud Android Library is available under MIT license
|
/* ownCloud Android Library is available under MIT license
|
||||||
* Copyright (C) 2020 ownCloud GmbH.
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -21,29 +21,21 @@
|
|||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package com.owncloud.android.lib.resources.files
|
|
||||||
|
|
||||||
import android.net.Uri
|
package com.owncloud.android.lib.resources.files.services
|
||||||
import com.owncloud.android.lib.common.OwnCloudClient
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
|
|
||||||
class RemoteFileUtil {
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
companion object {
|
import com.owncloud.android.lib.resources.Service
|
||||||
/**
|
|
||||||
* Retrieves a relative path from a remote file url
|
interface ChunkService : Service {
|
||||||
*
|
fun removeFile(
|
||||||
*
|
remotePath: String
|
||||||
* Example: url:port/remote.php/dav/files/username/Documents/text.txt => /Documents/text.txt
|
): RemoteOperationResult<Unit>
|
||||||
*
|
|
||||||
* @param url remote file url
|
fun moveFile(
|
||||||
* @param userId file owner
|
sourceRemotePath: String,
|
||||||
* @return remote relative path of the file
|
targetRemotePath: String,
|
||||||
*/
|
fileLastModificationTimestamp: String,
|
||||||
fun getRemotePathFromUrl(url: HttpUrl, userId: String): String? {
|
fileLength: Long
|
||||||
val davFilesPath = OwnCloudClient.WEBDAV_FILES_PATH_4_0 + userId
|
): RemoteOperationResult<Unit>
|
||||||
val absoluteDavPath = Uri.decode(url.encodedPath)
|
|
||||||
val pathToOc = absoluteDavPath.split(davFilesPath)[0]
|
|
||||||
return absoluteDavPath.replace(pathToOc + davFilesPath, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -25,8 +25,53 @@ package com.owncloud.android.lib.resources.files.services
|
|||||||
|
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
import com.owncloud.android.lib.resources.Service
|
import com.owncloud.android.lib.resources.Service
|
||||||
|
import com.owncloud.android.lib.resources.files.RemoteFile
|
||||||
|
|
||||||
interface FileService : Service {
|
interface FileService : Service {
|
||||||
fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult<Boolean>
|
|
||||||
fun getUrlToOpenInWeb(openWebEndpoint: String, fileId: String): RemoteOperationResult<String>
|
fun getUrlToOpenInWeb(openWebEndpoint: String, fileId: String): RemoteOperationResult<String>
|
||||||
|
|
||||||
|
fun checkPathExistence(
|
||||||
|
path: String,
|
||||||
|
isUserLogged: Boolean
|
||||||
|
): RemoteOperationResult<Boolean>
|
||||||
|
|
||||||
|
fun copyFile(
|
||||||
|
sourceRemotePath: String,
|
||||||
|
targetRemotePath: String,
|
||||||
|
): RemoteOperationResult<String>
|
||||||
|
|
||||||
|
fun createFolder(
|
||||||
|
remotePath: String,
|
||||||
|
createFullPath: Boolean,
|
||||||
|
isChunkFolder: Boolean = false
|
||||||
|
): RemoteOperationResult<Unit>
|
||||||
|
|
||||||
|
fun downloadFile(
|
||||||
|
remotePath: String,
|
||||||
|
localTempPath: String
|
||||||
|
): RemoteOperationResult<Unit>
|
||||||
|
|
||||||
|
fun moveFile(
|
||||||
|
sourceRemotePath: String,
|
||||||
|
targetRemotePath: String,
|
||||||
|
): RemoteOperationResult<Unit>
|
||||||
|
|
||||||
|
fun readFile(
|
||||||
|
remotePath: String
|
||||||
|
): RemoteOperationResult<RemoteFile>
|
||||||
|
|
||||||
|
fun refreshFolder(
|
||||||
|
remotePath: String
|
||||||
|
): RemoteOperationResult<ArrayList<RemoteFile>>
|
||||||
|
|
||||||
|
fun removeFile(
|
||||||
|
remotePath: String
|
||||||
|
): RemoteOperationResult<Unit>
|
||||||
|
|
||||||
|
fun renameFile(
|
||||||
|
oldName: String,
|
||||||
|
oldRemotePath: String,
|
||||||
|
newName: String,
|
||||||
|
isFolder: Boolean,
|
||||||
|
): RemoteOperationResult<Unit>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2021 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.resources.files.services.implementation
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.resources.files.chunks.MoveRemoteChunksFileOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.chunks.RemoveRemoteChunksFolderOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.services.ChunkService
|
||||||
|
|
||||||
|
class OCChunkService(override val client: OwnCloudClient) : ChunkService {
|
||||||
|
|
||||||
|
override fun removeFile(remotePath: String): RemoteOperationResult<Unit> =
|
||||||
|
RemoveRemoteChunksFolderOperation(remotePath = remotePath).execute(client)
|
||||||
|
|
||||||
|
override fun moveFile(
|
||||||
|
sourceRemotePath: String,
|
||||||
|
targetRemotePath: String,
|
||||||
|
fileLastModificationTimestamp: String,
|
||||||
|
fileLength: Long
|
||||||
|
): RemoteOperationResult<Unit> =
|
||||||
|
MoveRemoteChunksFileOperation(
|
||||||
|
sourceRemotePath = sourceRemotePath,
|
||||||
|
targetRemotePath = targetRemotePath,
|
||||||
|
fileLastModificationTimestamp = fileLastModificationTimestamp,
|
||||||
|
fileLength = fileLength,
|
||||||
|
).execute(client)
|
||||||
|
}
|
@ -26,12 +26,24 @@ package com.owncloud.android.lib.resources.files.services.implementation
|
|||||||
import com.owncloud.android.lib.common.OwnCloudClient
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation
|
import com.owncloud.android.lib.resources.files.CheckPathExistenceRemoteOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.CopyRemoteFileOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.CreateRemoteFolderOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.DownloadRemoteFileOperation
|
||||||
import com.owncloud.android.lib.resources.files.GetUrlToOpenInWebRemoteOperation
|
import com.owncloud.android.lib.resources.files.GetUrlToOpenInWebRemoteOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.MoveRemoteFileOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.ReadRemoteFileOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.ReadRemoteFolderOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.RemoteFile
|
||||||
|
import com.owncloud.android.lib.resources.files.RemoveRemoteFileOperation
|
||||||
|
import com.owncloud.android.lib.resources.files.RenameRemoteFileOperation
|
||||||
import com.owncloud.android.lib.resources.files.services.FileService
|
import com.owncloud.android.lib.resources.files.services.FileService
|
||||||
|
|
||||||
class OCFileService(override val client: OwnCloudClient) :
|
class OCFileService(override val client: OwnCloudClient) : FileService {
|
||||||
FileService {
|
|
||||||
override fun checkPathExistence(path: String, isUserLogged: Boolean): RemoteOperationResult<Boolean> =
|
override fun checkPathExistence(
|
||||||
|
path: String,
|
||||||
|
isUserLogged: Boolean
|
||||||
|
): RemoteOperationResult<Boolean> =
|
||||||
CheckPathExistenceRemoteOperation(
|
CheckPathExistenceRemoteOperation(
|
||||||
remotePath = path,
|
remotePath = path,
|
||||||
isUserLoggedIn = isUserLogged
|
isUserLoggedIn = isUserLogged
|
||||||
@ -40,4 +52,75 @@ class OCFileService(override val client: OwnCloudClient) :
|
|||||||
override fun getUrlToOpenInWeb(openWebEndpoint: String, fileId: String): RemoteOperationResult<String> =
|
override fun getUrlToOpenInWeb(openWebEndpoint: String, fileId: String): RemoteOperationResult<String> =
|
||||||
GetUrlToOpenInWebRemoteOperation(openWithWebEndpoint = openWebEndpoint, fileId = fileId).execute(client)
|
GetUrlToOpenInWebRemoteOperation(openWithWebEndpoint = openWebEndpoint, fileId = fileId).execute(client)
|
||||||
|
|
||||||
|
override fun copyFile(
|
||||||
|
sourceRemotePath: String,
|
||||||
|
targetRemotePath: String
|
||||||
|
): RemoteOperationResult<String> =
|
||||||
|
CopyRemoteFileOperation(
|
||||||
|
srcRemotePath = sourceRemotePath,
|
||||||
|
targetRemotePath = targetRemotePath
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun createFolder(
|
||||||
|
remotePath: String,
|
||||||
|
createFullPath: Boolean,
|
||||||
|
isChunkFolder: Boolean
|
||||||
|
): RemoteOperationResult<Unit> =
|
||||||
|
CreateRemoteFolderOperation(
|
||||||
|
remotePath = remotePath,
|
||||||
|
createFullPath = createFullPath,
|
||||||
|
isChunksFolder = isChunkFolder
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun downloadFile(
|
||||||
|
remotePath: String,
|
||||||
|
localTempPath: String
|
||||||
|
): RemoteOperationResult<Unit> =
|
||||||
|
DownloadRemoteFileOperation(
|
||||||
|
remotePath = remotePath,
|
||||||
|
localFolderPath = localTempPath
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun moveFile(
|
||||||
|
sourceRemotePath: String,
|
||||||
|
targetRemotePath: String,
|
||||||
|
): RemoteOperationResult<Unit> =
|
||||||
|
MoveRemoteFileOperation(
|
||||||
|
sourceRemotePath = sourceRemotePath,
|
||||||
|
targetRemotePath = targetRemotePath,
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun readFile(
|
||||||
|
remotePath: String
|
||||||
|
): RemoteOperationResult<RemoteFile> =
|
||||||
|
ReadRemoteFileOperation(
|
||||||
|
remotePath = remotePath
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun refreshFolder(
|
||||||
|
remotePath: String
|
||||||
|
): RemoteOperationResult<ArrayList<RemoteFile>> =
|
||||||
|
ReadRemoteFolderOperation(
|
||||||
|
remotePath = remotePath
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun removeFile(
|
||||||
|
remotePath: String
|
||||||
|
): RemoteOperationResult<Unit> =
|
||||||
|
RemoveRemoteFileOperation(
|
||||||
|
remotePath = remotePath
|
||||||
|
).execute(client)
|
||||||
|
|
||||||
|
override fun renameFile(
|
||||||
|
oldName: String,
|
||||||
|
oldRemotePath: String,
|
||||||
|
newName: String,
|
||||||
|
isFolder: Boolean
|
||||||
|
): RemoteOperationResult<Unit> =
|
||||||
|
RenameRemoteFileOperation(
|
||||||
|
oldName = oldName,
|
||||||
|
oldRemotePath = oldRemotePath,
|
||||||
|
newName = newName,
|
||||||
|
isFolder = isFolder
|
||||||
|
).execute(client)
|
||||||
}
|
}
|
||||||
|
@ -99,11 +99,11 @@ class GetRemoteShareesOperation
|
|||||||
.appendQueryParameter(PARAM_PER_PAGE, perPage.toString())
|
.appendQueryParameter(PARAM_PER_PAGE, perPage.toString())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun parseResponse(response: String): ShareeOcsResponse? {
|
private fun parseResponse(response: String?): ShareeOcsResponse? {
|
||||||
val moshi = Moshi.Builder().build()
|
val moshi = Moshi.Builder().build()
|
||||||
val type: Type = Types.newParameterizedType(CommonOcsResponse::class.java, ShareeOcsResponse::class.java)
|
val type: Type = Types.newParameterizedType(CommonOcsResponse::class.java, ShareeOcsResponse::class.java)
|
||||||
val adapter: JsonAdapter<CommonOcsResponse<ShareeOcsResponse>> = moshi.adapter(type)
|
val adapter: JsonAdapter<CommonOcsResponse<ShareeOcsResponse>> = moshi.adapter(type)
|
||||||
return adapter.fromJson(response)!!.ocs.data
|
return response?.let { adapter.fromJson(it)?.ocs?.data }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onResultUnsuccessful(
|
private fun onResultUnsuccessful(
|
||||||
@ -123,7 +123,7 @@ class GetRemoteShareesOperation
|
|||||||
private fun onRequestSuccessful(response: String?): RemoteOperationResult<ShareeOcsResponse> {
|
private fun onRequestSuccessful(response: String?): RemoteOperationResult<ShareeOcsResponse> {
|
||||||
val result = RemoteOperationResult<ShareeOcsResponse>(OK)
|
val result = RemoteOperationResult<ShareeOcsResponse>(OK)
|
||||||
Timber.d("Successful response: $response")
|
Timber.d("Successful response: $response")
|
||||||
result.data = parseResponse(response!!)
|
result.data = parseResponse(response)
|
||||||
Timber.d("*** Get Users or groups completed ")
|
Timber.d("*** Get Users or groups completed ")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.resources.webfinger
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.http.HttpConstants
|
||||||
|
import com.owncloud.android.lib.common.http.methods.nonwebdav.GetMethod
|
||||||
|
import com.owncloud.android.lib.common.http.methods.nonwebdav.HttpMethod
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperation
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.resources.webfinger.responses.WebfingerJrdResponse
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
class GetOCInstanceViaWebfingerOperation(
|
||||||
|
private val lockupServerDomain: String,
|
||||||
|
private val rel: String,
|
||||||
|
private val resource: String,
|
||||||
|
) : RemoteOperation<String>() {
|
||||||
|
|
||||||
|
private fun buildRequestUri() =
|
||||||
|
Uri.parse(lockupServerDomain).buildUpon()
|
||||||
|
.path(ENDPOINT_WEBFINGER_PATH)
|
||||||
|
.appendQueryParameter("rel", rel)
|
||||||
|
.appendQueryParameter("resource", resource)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun isSuccess(status: Int): Boolean = status == HttpConstants.HTTP_OK
|
||||||
|
|
||||||
|
private fun parseResponse(response: String): WebfingerJrdResponse {
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
val adapter = moshi.adapter(WebfingerJrdResponse::class.java)
|
||||||
|
return adapter.fromJson(response)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onResultUnsuccessful(
|
||||||
|
method: HttpMethod,
|
||||||
|
response: String?,
|
||||||
|
status: Int
|
||||||
|
): RemoteOperationResult<String> {
|
||||||
|
Timber.e("Failed requesting webfinger info")
|
||||||
|
if (response != null) {
|
||||||
|
Timber.e("*** status code: $status; response message: $response")
|
||||||
|
} else {
|
||||||
|
Timber.e("*** status code: $status")
|
||||||
|
}
|
||||||
|
return RemoteOperationResult<String>(method)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRequestSuccessful(rawResponse: String): RemoteOperationResult<String> {
|
||||||
|
val response = parseResponse(rawResponse)
|
||||||
|
for (i in response.links) {
|
||||||
|
if (i.rel == rel) {
|
||||||
|
val operationResult = RemoteOperationResult<String>(RemoteOperationResult.ResultCode.OK)
|
||||||
|
operationResult.data = i.href
|
||||||
|
return operationResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.e("Could not find ownCloud relevant information in webfinger response: $rawResponse")
|
||||||
|
throw java.lang.Exception("Could not find ownCloud relevant information in webfinger response")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run(client: OwnCloudClient): RemoteOperationResult<String> {
|
||||||
|
val requestUri = buildRequestUri()
|
||||||
|
val getMethod = GetMethod(URL(requestUri.toString()))
|
||||||
|
return try {
|
||||||
|
val status = client.executeHttpMethod(getMethod)
|
||||||
|
val response = getMethod.getResponseBodyAsString()!!
|
||||||
|
|
||||||
|
if (isSuccess(status)) {
|
||||||
|
onRequestSuccessful(response)
|
||||||
|
} else {
|
||||||
|
onResultUnsuccessful(getMethod, response, status)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Requesting webfinger info failed")
|
||||||
|
RemoteOperationResult<String>(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ENDPOINT_WEBFINGER_PATH = "/.well-known/webfinger"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.owncloud.android.lib.resources.webfinger.responses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class WebfingerJrdResponse(
|
||||||
|
val subject: String,
|
||||||
|
val links: List<LinkItem>
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class LinkItem(
|
||||||
|
val href: String,
|
||||||
|
val rel: String
|
||||||
|
)
|
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* ownCloud Android client application
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.webfinger.services
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
|
||||||
|
interface WebfingerService {
|
||||||
|
fun getInstanceFromWebfinger(
|
||||||
|
lookupServer: String,
|
||||||
|
username: String,
|
||||||
|
client: OwnCloudClient,
|
||||||
|
): RemoteOperationResult<String>
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* ownCloud Android client application
|
||||||
|
*
|
||||||
|
* Copyright (C) 2022 ownCloud GmbH.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License version 2,
|
||||||
|
* as published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.owncloud.android.lib.resources.webfinger.services.implementation
|
||||||
|
|
||||||
|
import com.owncloud.android.lib.common.OwnCloudClient
|
||||||
|
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||||
|
import com.owncloud.android.lib.resources.webfinger.GetOCInstanceViaWebfingerOperation
|
||||||
|
import com.owncloud.android.lib.resources.webfinger.services.WebfingerService
|
||||||
|
|
||||||
|
class OCWebfingerService : WebfingerService {
|
||||||
|
|
||||||
|
override fun getInstanceFromWebfinger(
|
||||||
|
lookupServer: String,
|
||||||
|
username: String,
|
||||||
|
client: OwnCloudClient,
|
||||||
|
): RemoteOperationResult<String> =
|
||||||
|
GetOCInstanceViaWebfingerOperation(lookupServer, OWNCLOUD_REL, username).execute(client)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val OWNCLOUD_REL = "http://webfinger.owncloud/rel/server-instance"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -63,31 +63,31 @@ class ShareeResponseTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `example response - ok - correct sturcture`() {
|
fun `example response - ok - correct sturcture`() {
|
||||||
val response = loadResponses(EXAMPLE_RESPONSE_JSON)!!
|
val response = loadResponses(EXAMPLE_RESPONSE_JSON)!!
|
||||||
assertEquals(2, response.ocs.data.groups.size)
|
assertEquals(2, response.ocs.data?.groups?.size)
|
||||||
assertEquals(0, response.ocs.data.remotes.size)
|
assertEquals(0, response.ocs.data?.remotes?.size)
|
||||||
assertEquals(2, response.ocs.data.users.size)
|
assertEquals(2, response.ocs.data?.users?.size)
|
||||||
assertEquals(0, response.ocs.data.exact?.groups?.size)
|
assertEquals(0, response.ocs.data?.exact?.groups?.size)
|
||||||
assertEquals(0, response.ocs.data.exact?.remotes?.size)
|
assertEquals(0, response.ocs.data?.exact?.remotes?.size)
|
||||||
assertEquals(1, response.ocs.data.exact?.users?.size)
|
assertEquals(1, response.ocs.data?.exact?.users?.size)
|
||||||
assertEquals("user1@user1.com", response.ocs.data.users.get(0).value.additionalInfo)
|
assertEquals("user1@user1.com", response.ocs.data?.users?.get(0)?.value?.additionalInfo)
|
||||||
assertNull(response.ocs.data.users[1].value.additionalInfo)
|
assertNull(response.ocs.data?.users?.get(1)?.value?.additionalInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `check empty response - ok - parsing ok`() {
|
fun `check empty response - ok - parsing ok`() {
|
||||||
val response = loadResponses(EMPTY_RESPONSE_JSON)!!
|
val response = loadResponses(EMPTY_RESPONSE_JSON)!!
|
||||||
assertTrue(response.ocs.data.exact?.groups?.isEmpty()!!)
|
assertTrue(response.ocs.data?.exact?.groups?.isEmpty()!!)
|
||||||
assertTrue(response.ocs.data.exact?.remotes?.isEmpty()!!)
|
assertTrue(response.ocs.data?.exact?.remotes?.isEmpty()!!)
|
||||||
assertTrue(response.ocs.data.exact?.users?.isEmpty()!!)
|
assertTrue(response.ocs.data?.exact?.users?.isEmpty()!!)
|
||||||
assertTrue(response.ocs.data.groups.isEmpty())
|
assertTrue(response.ocs.data?.groups?.isEmpty()!!)
|
||||||
assertTrue(response.ocs.data.remotes.isEmpty())
|
assertTrue(response.ocs.data?.remotes?.isEmpty()!!)
|
||||||
assertTrue(response.ocs.data.users.isEmpty())
|
assertTrue(response.ocs.data?.users?.isEmpty()!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val RESOURCES_PATH =
|
private const val RESOURCES_PATH =
|
||||||
"src/test/responses/com.owncloud.android.lib.resources.sharees.responses"
|
"src/test/responses/com.owncloud.android.lib.resources.sharees.responses"
|
||||||
val EXAMPLE_RESPONSE_JSON = "$RESOURCES_PATH/example_sharee_response.json"
|
const val EXAMPLE_RESPONSE_JSON = "$RESOURCES_PATH/example_sharee_response.json"
|
||||||
val EMPTY_RESPONSE_JSON = "$RESOURCES_PATH/empty_sharee_response.json"
|
const val EMPTY_RESPONSE_JSON = "$RESOURCES_PATH/empty_sharee_response.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
package com.owncloud.android.lib.resources.webfinger.responses
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.JsonDataException
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class WebfingerResponseTest {
|
||||||
|
lateinit var adapter: JsonAdapter<WebfingerJrdResponse>
|
||||||
|
|
||||||
|
private fun loadResponses(fileName: String) = adapter.fromJson(File(fileName).readText())
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun prepare() {
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
adapter = moshi.adapter(WebfingerJrdResponse::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check rel in too much information - ok`() {
|
||||||
|
val response = loadResponses(TOO_MUCH_INFORMATION_JSON)!!
|
||||||
|
Assert.assertEquals("https://gast.somedomain.de", response.links[0].href)
|
||||||
|
Assert.assertEquals("http://webfinger.owncloud/rel/server-instance", response.links[0].rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = JsonDataException::class)
|
||||||
|
fun `check key value pairs - ko - no href key`() {
|
||||||
|
val response = loadResponses(BROKEN_JSON)!!
|
||||||
|
Assert.assertEquals("https://gast.somedomain.de", response.links[0].href)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = JsonDataException::class)
|
||||||
|
fun `check key value pairs - ko - no rel key`() {
|
||||||
|
val response = loadResponses(BROKEN_JSON)!!
|
||||||
|
Assert.assertEquals("https://gast.somedomain.de", response.links[0].href)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RESOURCES_PATH =
|
||||||
|
"src/test/responses/com.owncloud.android.lib.resources.webfinger.responses"
|
||||||
|
private const val EXAMPLE_RESPONSE_JSON = "$RESOURCES_PATH/simple_response.json"
|
||||||
|
private const val TOO_MUCH_INFORMATION_JSON = "$RESOURCES_PATH/to_much_information_response.json"
|
||||||
|
private const val BROKEN_JSON = "$RESOURCES_PATH/broken_response.json"
|
||||||
|
private const val NOT_CONTAINING_RELEVANT_INFORMATION_JSON = "$RESOURCES_PATH/not_containing_relevant_info_response.json"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"subject": "acct:bob@example.com",
|
||||||
|
"aliases": [
|
||||||
|
"https://www.example.com/~bob/"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"http://example.com/ns/role": "employee"
|
||||||
|
},
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"lel": "http://webfinger.example/rel/profile-page",
|
||||||
|
"foo": "https://www.example.com/~bob/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"subject": "acct:peter.sine@gurken.xxx",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"rel": "http://webfinger.example/rel/businesscard",
|
||||||
|
"href": "https://www.example.com/~bob/bob.vcf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"href": "https://gast.somedomain.de",
|
||||||
|
"rel": "http://webfinger.owncloud/rel/server-instance"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subject": "acct:peter.sine@gurken.xxx"
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"subject": "acct:peter.sine@gurken.xxx",
|
||||||
|
"aliases": [
|
||||||
|
"https://www.example.com/~bob/"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"http://example.com/ns/role": "employee"
|
||||||
|
},
|
||||||
|
"gurken": {
|
||||||
|
"whatever": 42
|
||||||
|
},
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"gurken": "sallat",
|
||||||
|
"href": "https://gast.somedomain.de",
|
||||||
|
"rel": "http://webfinger.owncloud/rel/server-instance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gurken": "sallat",
|
||||||
|
"rel": "http://webfinger.example/rel/businesscard",
|
||||||
|
"href": "https://www.example.com/~bob/bob.vcf"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user