diff --git a/library/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.kt b/library/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.kt index 23edd98917..2cb6031739 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.kt +++ b/library/src/main/java/com/owncloud/android/lib/common/network/WebdavEntry.kt @@ -1,6 +1,7 @@ /* * Nextcloud Android Library * + * SPDX-FileCopyrightText: 2025 TSI-mc * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2023 Álvaro Brey @@ -679,6 +680,11 @@ class WebdavEntry constructor( const val SHAREES_SHARE_TYPE = "type" const val PROPERTY_QUOTA_USED_BYTES = "quota-used-bytes" const val PROPERTY_QUOTA_AVAILABLE_BYTES = "quota-available-bytes" + const val PROPERTY_LAST_PHOTO = "last-photo" + const val PROPERTY_NB_ITEMS = "nbItems" + const val PROPERTY_LOCATION = "location" + const val PROPERTY_DATE_RANGE = "dateRange" + const val PROPERTY_COLLABORATORS = "collaborators" private const val IS_ENCRYPTED = "1" private const val CODE_PROP_NOT_FOUND = 404 } diff --git a/library/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java b/library/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java index e83afbbcd9..c900f6d163 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java +++ b/library/src/main/java/com/owncloud/android/lib/common/network/WebdavUtils.java @@ -1,6 +1,7 @@ /* * Nextcloud Android Library * + * SPDX-FileCopyrightText: 2025 TSI-mc * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2023 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey @@ -221,6 +222,20 @@ public static DavPropertyNameSet getChunksPropSet() { return propSet; } + public static DavPropertyNameSet getAlbumPropSet() { + DavPropertyNameSet propertySet = new DavPropertyNameSet(); + Namespace ncNamespace = Namespace.getNamespace("nc",WebdavEntry.NAMESPACE_NC); + + propertySet.add(DavPropertyName.create(WebdavEntry.PROPERTY_LAST_PHOTO,ncNamespace)); + propertySet.add(DavPropertyName.create(WebdavEntry.PROPERTY_NB_ITEMS, ncNamespace)); + propertySet.add(DavPropertyName.create(WebdavEntry.PROPERTY_LOCATION, ncNamespace)); + propertySet.add(DavPropertyName.create(WebdavEntry.PROPERTY_DATE_RANGE, ncNamespace)); + propertySet.add(DavPropertyName.create(WebdavEntry.PROPERTY_COLLABORATORS, ncNamespace)); + + return propertySet; + } + + /** * * @param rawEtag diff --git a/library/src/main/java/com/owncloud/android/lib/common/utils/WebDavFileUtils.java b/library/src/main/java/com/owncloud/android/lib/common/utils/WebDavFileUtils.java index bb19408b84..0ca9d16013 100644 --- a/library/src/main/java/com/owncloud/android/lib/common/utils/WebDavFileUtils.java +++ b/library/src/main/java/com/owncloud/android/lib/common/utils/WebDavFileUtils.java @@ -1,12 +1,15 @@ /* * Nextcloud Android Library * + * SPDX-FileCopyrightText: 2025 TSI-mc * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2017 Mario Danic * SPDX-License-Identifier: MIT */ package com.owncloud.android.lib.common.utils; +import android.net.Uri; + import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.network.WebdavEntry; import com.owncloud.android.lib.resources.files.model.RemoteFile; @@ -15,6 +18,8 @@ import org.apache.jackrabbit.webdav.MultiStatusResponse; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * WebDav helper. @@ -59,4 +64,33 @@ public ArrayList readData(MultiStatus remoteData, return mFolderAndFiles; } + + /** + * Read the data retrieved from the server about the contents of the target folder + * + * @param remoteData Full response got from the server with the data of the target + * folder and its direct children. + * @param client Client instance to the remote server where the data were + * retrieved. + * @return + */ + public List readAlbumData(MultiStatus remoteData, OwnCloudClient client) { + String baseUrl = client.getBaseUri() + "/remote.php/dav/photos/" + client.getUserId(); + String encodedPath = Uri.parse(baseUrl).getEncodedPath(); + if (encodedPath == null) { + return Collections.emptyList(); + } + + final var responses = remoteData.getResponses(); + List files = new ArrayList<>(Math.max(0, responses.length - 1)); + + // reading from 1 as 0th item will be just the root album path + for (int i = 1; i < responses.length; i++) { + WebdavEntry entry = new WebdavEntry(responses[i], encodedPath); + RemoteFile remoteFile = new RemoteFile(entry); + files.add(remoteFile); + } + + return files; + } } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/CopyFileToAlbumRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/CopyFileToAlbumRemoteOperation.kt new file mode 100644 index 0000000000..9272a98208 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/CopyFileToAlbumRemoteOperation.kt @@ -0,0 +1,154 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import android.util.Log +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +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 org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.DavException +import org.apache.jackrabbit.webdav.Status +import org.apache.jackrabbit.webdav.client.methods.CopyMethod +import java.io.IOException + +/** + * 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. + */ +class CopyFileToAlbumRemoteOperation @JvmOverloads constructor( + private val mSrcRemotePath: String, + private val mTargetRemotePath: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut +) : + RemoteOperation() { + /** + * Performs the operation. + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + /** check parameters */ + + var result: RemoteOperationResult + if (mTargetRemotePath == mSrcRemotePath) { + // nothing to do! + result = RemoteOperationResult(ResultCode.OK) + } else if (mTargetRemotePath.startsWith(mSrcRemotePath)) { + result = RemoteOperationResult(ResultCode.INVALID_COPY_INTO_DESCENDANT) + } else { + /** perform remote operation */ + var copyMethod: CopyMethod? = null + try { + copyMethod = CopyMethod( + client.getFilesDavUri(this.mSrcRemotePath), + "${client.baseUri}/remote.php/dav/photos/${client.userId}/albums${ + WebdavUtils.encodePath( + mTargetRemotePath + ) + }", + false + ) + val status = client.executeMethod( + copyMethod, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + + /** process response */ + result = when (status) { + HttpStatus.SC_MULTI_STATUS -> processPartialError(copyMethod) + HttpStatus.SC_PRECONDITION_FAILED -> { + client.exhaustResponse(copyMethod.responseBodyAsStream) + RemoteOperationResult(ResultCode.INVALID_OVERWRITE) + } + + else -> { + client.exhaustResponse(copyMethod.responseBodyAsStream) + RemoteOperationResult(isSuccess(status), copyMethod) + } + } + + Log.i( + TAG, + "Copy $mSrcRemotePath to $mTargetRemotePath : ${result.logMessage}" + ) + } catch (e: Exception) { + result = RemoteOperationResult(e) + Log.e( + TAG, + "Copy $mSrcRemotePath to $mTargetRemotePath : ${result.logMessage}", e + ) + } finally { + copyMethod?.releaseConnection() + } + } + + return result + } + + /** + * Analyzes a multistatus response from the OC server to generate an appropriate result. + * + * + * In WebDAV, a COPY request on collections (folders) can be PARTIALLY successful: some + * children are copied, some other aren't. + * + * + * According to the WebDAV specification, a multistatus response SHOULD NOT include partial + * successes (201, 204) nor for descendants of already failed children (424) in the response + * entity. But SHOULD NOT != MUST NOT, so take carefully. + * + * @param copyMethod Copy operation just finished with a multistatus response + * @return A result for the [CopyFileToAlbumRemoteOperation] caller + * @throws java.io.IOException If the response body could not be parsed + * @throws org.apache.jackrabbit.webdav.DavException If the status code is other than MultiStatus or if obtaining + * the response XML document fails + */ + @Throws(IOException::class, DavException::class) + private fun processPartialError(copyMethod: CopyMethod): RemoteOperationResult { + // Adding a list of failed descendants to the result could be interesting; or maybe not. + // For the moment, let's take the easy way. + /** check that some error really occurred */ + + val responses = copyMethod.responseBodyAsMultiStatus.responses + var status: Array? + var failFound = false + var i = 0 + while (i < responses.size && !failFound) { + status = responses[i].status + failFound = (!status.isNullOrEmpty() && status[0].statusCode > FAILED_STATUS_CODE + ) + i++ + } + val result: RemoteOperationResult = if (failFound) { + RemoteOperationResult(ResultCode.PARTIAL_COPY_DONE) + } else { + RemoteOperationResult(true, copyMethod) + } + + return result + } + + private fun isSuccess(status: Int): Boolean { + return status == HttpStatus.SC_CREATED || status == HttpStatus.SC_NO_CONTENT + } + + companion object { + private val TAG: String = CopyFileToAlbumRemoteOperation::class.java.simpleName + private const val FAILED_STATUS_CODE = 299 + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/CreateNewAlbumRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/CreateNewAlbumRemoteOperation.kt new file mode 100644 index 0000000000..6d69d53a05 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/CreateNewAlbumRemoteOperation.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +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.Log_OC +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.client.methods.MkColMethod + +class CreateNewAlbumRemoteOperation + @JvmOverloads + constructor( + val newAlbumName: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation() { + /** + * Performs the operation. + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + var mkCol: MkColMethod? = null + var result: RemoteOperationResult + try { + mkCol = + MkColMethod( + "${client.baseUri}/remote.php/dav/photos/${client.userId}/albums${ + WebdavUtils.encodePath( + newAlbumName + ) + }" + ) + client.executeMethod( + mkCol, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + if (HttpStatus.SC_METHOD_NOT_ALLOWED == mkCol.statusCode) { + result = + RemoteOperationResult(RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS) + } else { + result = RemoteOperationResult(mkCol.succeeded(), mkCol) + result.resultData = null + } + + Log_OC.d(TAG, "Create album $newAlbumName : ${result.logMessage}") + client.exhaustResponse(mkCol.responseBodyAsStream) + } catch (e: Exception) { + result = RemoteOperationResult(e) + Log_OC.e(TAG, "Create album $newAlbumName : ${result.logMessage}", e) + } finally { + mkCol?.releaseConnection() + } + + return result + } + + companion object { + private val TAG: String = CreateNewAlbumRemoteOperation::class.java.simpleName + } + } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/PhotoAlbumEntry.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/PhotoAlbumEntry.kt new file mode 100644 index 0000000000..168ec0d6be --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/PhotoAlbumEntry.kt @@ -0,0 +1,96 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.albums + +import com.owncloud.android.lib.common.network.WebdavEntry +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.MultiStatusResponse +import org.apache.jackrabbit.webdav.property.DavPropertyName +import org.apache.jackrabbit.webdav.property.DavPropertySet +import org.apache.jackrabbit.webdav.xml.Namespace +import org.json.JSONException +import org.json.JSONObject +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class PhotoAlbumEntry( + response: MultiStatusResponse +) { + val href: String + val lastPhoto: Long + val nbItems: Int + val location: String? + private val dateRange: String? + + companion object { + private val dateFormat = SimpleDateFormat("MMM yyyy", Locale.US) + private const val MILLIS = 1000L + } + + init { + + href = response.href + + val properties = response.getProperties(HttpStatus.SC_OK) + + this.lastPhoto = parseLong(parseString(properties, WebdavEntry.PROPERTY_LAST_PHOTO)) + this.nbItems = parseInt(parseString(properties, WebdavEntry.PROPERTY_NB_ITEMS)) + this.location = parseString(properties, WebdavEntry.PROPERTY_LOCATION) + this.dateRange = parseString(properties, WebdavEntry.PROPERTY_DATE_RANGE) + } + + private fun parseString( + props: DavPropertySet, + name: String + ): String? { + val propName = DavPropertyName.create(name, Namespace.getNamespace("nc", WebdavEntry.NAMESPACE_NC)) + val prop = props[propName] + return if (prop != null && prop.value != null) prop.value.toString() else null + } + + private fun parseInt(value: String?): Int = + try { + value?.toInt() ?: 0 + } catch (_: NumberFormatException) { + 0 + } + + private fun parseLong(value: String?): Long = + try { + value?.toLong() ?: 0L + } catch (_: NumberFormatException) { + 0L + } + + val albumName: String + get() { + return href + .removeSuffix("/") + .substringAfterLast("/") + .takeIf { it.isNotEmpty() } ?: "" + } + + val createdDate: String + get() { + val currentDate = Date(System.currentTimeMillis()) + + return try { + val obj = JSONObject(dateRange ?: return dateFormat.format(currentDate)) + val startTimestamp = obj.optLong("start", 0) + if (startTimestamp > 0) { + dateFormat.format(Date(startTimestamp * MILLIS)) + } else { + dateFormat.format(currentDate) + } + } catch (e: JSONException) { + e.printStackTrace() + dateFormat.format(currentDate) + } + } +} diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/ReadAlbumItemsRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/ReadAlbumItemsRemoteOperation.kt new file mode 100644 index 0000000000..40deed2e70 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/ReadAlbumItemsRemoteOperation.kt @@ -0,0 +1,95 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +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.Log_OC +import com.owncloud.android.lib.common.utils.WebDavFileUtils +import com.owncloud.android.lib.resources.files.model.RemoteFile +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.DavConstants +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod + +class ReadAlbumItemsRemoteOperation + @JvmOverloads + constructor( + private val mRemotePath: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation>() { + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult> { + var result: RemoteOperationResult>? = null + var query: PropFindMethod? = null + val url = "${client.baseUri}/remote.php/dav/photos/${client.userId}/albums${ + WebdavUtils.encodePath( + mRemotePath + ) + }" + try { + // remote request + query = + PropFindMethod( + url, + WebdavUtils.getAllPropSet(), // PropFind Properties + DavConstants.DEPTH_1 + ) + val status = + client.executeMethod( + query, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + + // check and process response + val isSuccess = (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) + + result = + if (isSuccess) { + // get data from remote folder + val dataInServer = query.responseBodyAsMultiStatus + val mFolderAndFiles = WebDavFileUtils().readAlbumData(dataInServer, client) + + // Result of the operation + RemoteOperationResult>(true, query).apply { + // Add data to the result + resultData = mFolderAndFiles + } + } else { + // synchronization failed + client.exhaustResponse(query.responseBodyAsStream) + RemoteOperationResult(false, query) + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + } finally { + query?.releaseConnection() + + result = result ?: RemoteOperationResult>(Exception("unknown error")).also { + Log_OC.e(TAG, "Synchronized $mRemotePath: failed") + } + if (result.isSuccess) { + Log_OC.i(TAG, "Synchronized $mRemotePath : ${result.logMessage}") + } else if (result.isException) { + Log_OC.e(TAG, "Synchronized $mRemotePath : ${result.logMessage}", result.exception) + } else { + Log_OC.e(TAG, "Synchronized $mRemotePath : ${result.logMessage}") + } + } + + return result + } + + companion object { + private val TAG: String = ReadAlbumItemsRemoteOperation::class.java.simpleName + } + } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/ReadAlbumsRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/ReadAlbumsRemoteOperation.kt new file mode 100644 index 0000000000..c472b34e8c --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/ReadAlbumsRemoteOperation.kt @@ -0,0 +1,74 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import android.text.TextUtils +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +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.Log_OC +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.DavConstants +import org.apache.jackrabbit.webdav.client.methods.PropFindMethod + +class ReadAlbumsRemoteOperation + @JvmOverloads + constructor( + private val mAlbumRemotePath: String? = null, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation>() { + /** + * Performs the operation. + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult> { + var propfind: PropFindMethod? = null + var result: RemoteOperationResult> + var url = "${client.baseUri}/remote.php/dav/photos/${client.userId}/albums" + if (!TextUtils.isEmpty(mAlbumRemotePath)) { + url += WebdavUtils.encodePath(mAlbumRemotePath) + } + try { + propfind = PropFindMethod(url, WebdavUtils.getAlbumPropSet(), DavConstants.DEPTH_1) + val status = + client.executeMethod( + propfind, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + val isSuccess = status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK + if (isSuccess) { + val albumsList = + propfind.responseBodyAsMultiStatus.responses + .filter { it.status[0].statusCode == HttpStatus.SC_OK } + .map { res -> PhotoAlbumEntry(res) } + result = RemoteOperationResult>(true, propfind) + result.resultData = albumsList + } else { + result = RemoteOperationResult>(false, propfind) + client.exhaustResponse(propfind.responseBodyAsStream) + } + } catch (e: Exception) { + result = RemoteOperationResult>(e) + Log_OC.e(TAG, "Read album failed: ${result.logMessage}", result.exception) + } finally { + propfind?.releaseConnection() + } + + return result + } + + companion object { + private val TAG: String = ReadAlbumsRemoteOperation::class.java.simpleName + } + } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/RemoveAlbumFileRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/RemoveAlbumFileRemoteOperation.kt new file mode 100644 index 0000000000..51c5ee2efc --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/RemoveAlbumFileRemoteOperation.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import android.net.Uri +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.RemoveFileRemoteOperation +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.client.methods.DeleteMethod + +class RemoveAlbumFileRemoteOperation + @JvmOverloads + constructor( + private val mRemotePath: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation() { + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + var delete: DeleteMethod? = null + val webDavUrl = "${client.davUri}/photos/" + val encodedPath = ("${client.userId}${Uri.encode(this.mRemotePath)}").replace("%2F", "/") + val fullFilePath = "$webDavUrl$encodedPath" + + try { + delete = DeleteMethod(fullFilePath) + val status = + client.executeMethod( + delete, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + delete.responseBodyAsString + result = + RemoteOperationResult( + delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, + delete + ) + Log_OC.i(TAG, "Remove ${this.mRemotePath} : ${result.logMessage}") + } catch (e: Exception) { + result = RemoteOperationResult(e) + Log_OC.e(TAG, "Remove ${this.mRemotePath} : ${result.logMessage}", e) + } finally { + delete?.releaseConnection() + } + + return result + } + + companion object { + private val TAG: String = RemoveFileRemoteOperation::class.java.simpleName + } + } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/RemoveAlbumRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/RemoveAlbumRemoteOperation.kt new file mode 100644 index 0000000000..b9114ac8f2 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/RemoveAlbumRemoteOperation.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +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.Log_OC +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.client.methods.DeleteMethod + +class RemoveAlbumRemoteOperation + @JvmOverloads + constructor( + private val albumName: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation() { + /** + * Performs the operation. + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + var delete: DeleteMethod? = null + + try { + delete = + DeleteMethod( + "${client.baseUri}/remote.php/dav/photos/${client.userId}/albums${ + WebdavUtils.encodePath( + albumName + ) + }" + ) + val status = + client.executeMethod( + delete, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + delete.responseBodyAsString + result = + RemoteOperationResult( + delete.succeeded() || status == HttpStatus.SC_NOT_FOUND, + delete + ) + Log_OC.i(TAG, "Remove ${this.albumName} : ${result.logMessage}") + } catch (e: Exception) { + result = RemoteOperationResult(e) + Log_OC.e(TAG, "Remove ${this.albumName} : ${result.logMessage}", e) + } finally { + delete?.releaseConnection() + } + + return result + } + + companion object { + private val TAG: String = RemoveAlbumRemoteOperation::class.java.simpleName + } + } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/RenameAlbumRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/RenameAlbumRemoteOperation.kt new file mode 100644 index 0000000000..6dff04e913 --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/RenameAlbumRemoteOperation.kt @@ -0,0 +1,77 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +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.Log_OC +import org.apache.jackrabbit.webdav.client.methods.MoveMethod + +class RenameAlbumRemoteOperation + @JvmOverloads + constructor( + private val mOldRemotePath: String, + val newAlbumName: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation() { + /** + * Performs the operation. + * + * @param client Client object to communicate with the remote ownCloud server. + */ + @Deprecated("Deprecated in Java") + @Suppress("TooGenericExceptionCaught") + override fun run(client: OwnCloudClient): RemoteOperationResult? { + var result: RemoteOperationResult? = null + var move: MoveMethod? = null + val url = "${client.baseUri}/remote.php/dav/photos/${client.userId}/albums" + try { + if (this.newAlbumName != this.mOldRemotePath) { + move = + MoveMethod( + "$url${WebdavUtils.encodePath(mOldRemotePath)}", + "$url${ + WebdavUtils.encodePath( + newAlbumName + ) + }", + true + ) + client.executeMethod( + move, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + result = RemoteOperationResult(move.succeeded(), move) + Log_OC.i( + TAG, + "Rename ${this.mOldRemotePath} to ${this.newAlbumName} : ${result.logMessage}" + ) + client.exhaustResponse(move.responseBodyAsStream) + } + } catch (e: Exception) { + result = RemoteOperationResult(e) + Log_OC.e( + TAG, + "Rename ${this.mOldRemotePath} to ${this.newAlbumName} : ${result.logMessage}", + e + ) + } finally { + move?.releaseConnection() + } + + return result + } + + companion object { + private val TAG: String = RenameAlbumRemoteOperation::class.java.simpleName + } + } diff --git a/library/src/main/java/com/owncloud/android/lib/resources/albums/ToggleAlbumFavoriteRemoteOperation.kt b/library/src/main/java/com/owncloud/android/lib/resources/albums/ToggleAlbumFavoriteRemoteOperation.kt new file mode 100644 index 0000000000..b2253c69ca --- /dev/null +++ b/library/src/main/java/com/owncloud/android/lib/resources/albums/ToggleAlbumFavoriteRemoteOperation.kt @@ -0,0 +1,76 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ +package com.owncloud.android.lib.resources.albums + +import android.net.Uri +import com.nextcloud.common.SessionTimeOut +import com.nextcloud.common.defaultSessionTimeOut +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.network.WebdavEntry +import com.owncloud.android.lib.common.operations.RemoteOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import org.apache.commons.httpclient.HttpStatus +import org.apache.jackrabbit.webdav.client.methods.PropPatchMethod +import org.apache.jackrabbit.webdav.property.DavPropertyNameSet +import org.apache.jackrabbit.webdav.property.DavPropertySet +import org.apache.jackrabbit.webdav.property.DefaultDavProperty +import org.apache.jackrabbit.webdav.xml.Namespace +import java.io.IOException + +class ToggleAlbumFavoriteRemoteOperation + @JvmOverloads + constructor( + private val makeItFavorited: Boolean, + private val filePath: String, + private val sessionTimeOut: SessionTimeOut = defaultSessionTimeOut + ) : RemoteOperation() { + @Deprecated("Deprecated in Java") + override fun run(client: OwnCloudClient): RemoteOperationResult { + var result: RemoteOperationResult + var propPatchMethod: PropPatchMethod? = null + val newProps = DavPropertySet() + val removeProperties = DavPropertyNameSet() + if (this.makeItFavorited) { + val favoriteProperty = + DefaultDavProperty( + "oc:favorite", + "1", + Namespace.getNamespace(WebdavEntry.NAMESPACE_OC) + ) + newProps.add(favoriteProperty) + } else { + removeProperties.add("oc:favorite", Namespace.getNamespace(WebdavEntry.NAMESPACE_OC)) + } + + val webDavUrl = "${client.davUri}/photos/" + val encodedPath = ("${client.userId}${Uri.encode(this.filePath)}").replace("%2F", "/") + val fullFilePath = "$webDavUrl$encodedPath" + + try { + propPatchMethod = PropPatchMethod(fullFilePath, newProps, removeProperties) + val status = + client.executeMethod( + propPatchMethod, + sessionTimeOut.readTimeOut, + sessionTimeOut.connectionTimeOut + ) + val isSuccess = (status == HttpStatus.SC_MULTI_STATUS || status == HttpStatus.SC_OK) + if (isSuccess) { + result = RemoteOperationResult(true, status, propPatchMethod.responseHeaders) + } else { + client.exhaustResponse(propPatchMethod.responseBodyAsStream) + result = RemoteOperationResult(false, status, propPatchMethod.responseHeaders) + } + } catch (e: IOException) { + result = RemoteOperationResult(e) + } finally { + propPatchMethod?.releaseConnection() + } + + return result + } + } diff --git a/library/src/test/java/com/owncloud/android/lib/resources/albums/PhotoAlbumEntryTest.kt b/library/src/test/java/com/owncloud/android/lib/resources/albums/PhotoAlbumEntryTest.kt new file mode 100644 index 0000000000..f80c20d70a --- /dev/null +++ b/library/src/test/java/com/owncloud/android/lib/resources/albums/PhotoAlbumEntryTest.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud Android Library + * + * SPDX-FileCopyrightText: 2025 TSI-mc + * SPDX-License-Identifier: MIT + */ + +package com.owncloud.android.lib.resources.albums + +import org.apache.jackrabbit.webdav.MultiStatusResponse +import org.junit.Assert.assertEquals +import org.junit.Test + +class PhotoAlbumEntryTest { + @Test + fun testAlbumName_withTrailingSlash() { + val entry = createTestEntry("/remote.php/dav/photos/user_id/albums/vacation2024/") + assertEquals("vacation2024", entry.albumName) + } + + @Test + fun testAlbumName_withoutTrailingSlash() { + val entry = createTestEntry("/remote.php/dav/photos/user_id/albums/vacation2024") + assertEquals("vacation2024", entry.albumName) + } + + @Test + fun testAlbumName_nestedPath() { + val entry = createTestEntry("/remote.php/dav/photos/user_id/albums/travel/europe/") + assertEquals("europe", entry.albumName) + } + + @Test + fun testAlbumName_singleSlash() { + val entry = createTestEntry("/") + assertEquals("", entry.albumName) + } + + @Test + fun testAlbumName_onlySlashes() { + val entry = createTestEntry("///") + assertEquals("", entry.albumName) + } + + @Test + fun testAlbumName_noSlash() { + val entry = createTestEntry("holiday") + assertEquals("holiday", entry.albumName) + } + + // Helper method to create a stub entry + private fun createTestEntry(href: String): PhotoAlbumEntry { + val response = MultiStatusResponse(href, 200) + return PhotoAlbumEntry(response) + } +}