@@ -9,6 +9,8 @@ package com.owncloud.android.lib.resources.files
99import android.os.Build
1010import androidx.annotation.RequiresApi
1111import com.nextcloud.common.NextcloudClient
12+ import com.nextcloud.common.SessionTimeOut
13+ import com.nextcloud.common.defaultSessionTimeOut
1214import com.nextcloud.operations.GetMethod
1315import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
1416import com.owncloud.android.lib.common.network.WebdavUtils
@@ -27,128 +29,162 @@ import java.util.concurrent.atomic.AtomicBoolean
2729
2830@Suppress(" NestedBlockDepth" , " TooGenericExceptionCaught" , " ThrowsCount" )
2931@RequiresApi(Build .VERSION_CODES .O )
30- class DownloadFileRemoteOperation (
31- private val remotePath : String ,
32- private val temporalFolderPath : String?
33- ) : RemoteOperation<Any>() {
34- private val dataTransferListeners = ConcurrentHashMap .newKeySet<OnDatatransferProgressListener >()
35- private val cancellationRequested = AtomicBoolean (false )
36- var modificationTimestamp: Long = 0
37- private set
38- var etag: String = " "
39- private set
40-
41- @Suppress(" DEPRECATION" )
42- override fun run (client : NextcloudClient ): RemoteOperationResult <Any > {
43- val targetPath = Paths .get(tmpPath)
44- return try {
45- val parent = targetPath.parent ? : throw IOException (" No parent directory for: $targetPath " )
46- Files .createDirectories(parent)
47- val getMethod = GetMethod (client.getFilesDavUri(remotePath), false )
48- val status = downloadFile(getMethod, client, targetPath)
49- RemoteOperationResult <Any >(isSuccess(status), getMethod).also {
50- Log_OC .i(TAG , " Download of $remotePath to $targetPath : ${it.logMessage} " )
51- }
52- } catch (e: Exception ) {
53- RemoteOperationResult <Any >(e).also {
54- Log_OC .e(TAG , " Download of $remotePath to $targetPath : ${it.logMessage} " , e)
32+ class DownloadFileRemoteOperation
33+ @JvmOverloads
34+ constructor (
35+ private val remotePath: String ,
36+ private val temporalFolderPath: String? ,
37+ private val fileSizeInBytes: Long? = null
38+ ) : RemoteOperation <Any >() {
39+ private val dataTransferListeners = ConcurrentHashMap .newKeySet<OnDatatransferProgressListener >()
40+ private val cancellationRequested = AtomicBoolean (false )
41+ var modificationTimestamp: Long = 0
42+ private set
43+ var etag: String = " "
44+ private set
45+
46+ @Suppress(" DEPRECATION" )
47+ override fun run (client : NextcloudClient ): RemoteOperationResult <Any > {
48+ val targetPath = Paths .get(tmpPath)
49+ return try {
50+ val parent = targetPath.parent ? : throw IOException (" No parent directory for: $targetPath " )
51+ Files .createDirectories(parent)
52+ val getMethod = GetMethod (client.getFilesDavUri(remotePath), false )
53+ val status = downloadFile(getMethod, client, targetPath)
54+ RemoteOperationResult <Any >(isSuccess(status), getMethod).also {
55+ Log_OC .i(TAG , " Download of $remotePath to $targetPath : ${it.logMessage} " )
56+ }
57+ } catch (e: Exception ) {
58+ RemoteOperationResult <Any >(e).also {
59+ Log_OC .e(TAG , " Download of $remotePath to $targetPath : ${it.logMessage} " , e)
60+ }
5561 }
5662 }
57- }
5863
59- // region private methods
60- @Throws(IOException ::class , OperationCancelledException ::class , CreateLocalFileException ::class )
61- private fun downloadFile (
62- getMethod : GetMethod ,
63- client : NextcloudClient ,
64- targetPath : Path
65- ): Int {
66- val status = client.execute(getMethod)
67- if (! isSuccess(status)) {
68- getMethod.releaseConnection()
64+ // region private methods
65+ @Throws(IOException ::class , OperationCancelledException ::class , CreateLocalFileException ::class )
66+ private fun downloadFile (
67+ getMethod : GetMethod ,
68+ client : NextcloudClient ,
69+ targetPath : Path
70+ ): Int {
71+ val sessionTimeOut = calculateSessionTimeOut(fileSizeInBytes)
72+ val downloadClient = client.withSessionTimeOut(sessionTimeOut)
73+ val status = downloadClient.execute(getMethod)
74+ if (! isSuccess(status)) {
75+ getMethod.releaseConnection()
76+ return status
77+ }
78+
79+ try {
80+ writeResponseToFile(getMethod, targetPath)
81+ readMetadata(getMethod)
82+ } finally {
83+ getMethod.releaseConnection()
84+ }
85+
6986 return status
7087 }
7188
72- try {
73- writeResponseToFile(getMethod, targetPath)
74- readMetadata(getMethod)
75- } finally {
76- getMethod.releaseConnection()
89+ @Suppress(" ReturnCount" )
90+ private fun calculateSessionTimeOut (fileSizeInBytes : Long? ): SessionTimeOut {
91+ fileSizeInBytes ? : return defaultSessionTimeOut
92+ if (fileSizeInBytes <= 0 ) return defaultSessionTimeOut
93+ val readTimeOut =
94+ (READ_TIMEOUT_PER_GB * fileSizeInBytes / BYTES_PER_GB_LONG )
95+ .coerceIn(READ_TIMEOUT_MIN , READ_TIMEOUT_MAX )
96+ .toInt()
97+ return SessionTimeOut (readTimeOut = readTimeOut, connectionTimeOut = CONNECTION_TIMEOUT )
7798 }
7899
79- return status
80- }
81-
82- private fun writeResponseToFile (
83- getMethod : GetMethod ,
84- targetPath : Path
85- ) {
86- val responseStream =
87- getMethod.getResponseBodyAsStream()
88- ? : throw IOException (" Empty response body for $remotePath " )
89-
90- val outputStream =
91- try {
92- Files .newOutputStream(targetPath)
93- } catch (ex: IOException ) {
94- Log_OC .e(TAG , " Error creating file $targetPath " , ex)
95- throw CreateLocalFileException (targetPath.toString(), ex)
96- } catch (ex: SecurityException ) {
97- Log_OC .e(TAG , " Error creating file $targetPath " , ex)
98- throw CreateLocalFileException (targetPath.toString(), ex)
99- }
100+ private fun writeResponseToFile (
101+ getMethod : GetMethod ,
102+ targetPath : Path
103+ ) {
104+ val responseStream =
105+ getMethod.getResponseBodyAsStream()
106+ ? : throw IOException (" Empty response body for $remotePath " )
107+
108+ val outputStream =
109+ try {
110+ Files .newOutputStream(targetPath)
111+ } catch (ex: IOException ) {
112+ Log_OC .e(TAG , " Error creating file $targetPath " , ex)
113+ throw CreateLocalFileException (targetPath.toString(), ex)
114+ } catch (ex: SecurityException ) {
115+ Log_OC .e(TAG , " Error creating file $targetPath " , ex)
116+ throw CreateLocalFileException (targetPath.toString(), ex)
117+ }
100118
101- BufferedInputStream (responseStream).use { bis ->
102- outputStream.use { fos ->
103- val buffer = ByteArray (BUFFER_SIZE )
104- var bytesRead: Int
105- while (bis.read(buffer).also { bytesRead = it } != - 1 ) {
106- if (cancellationRequested.get()) throw OperationCancelledException ()
107- fos.write(buffer, 0 , bytesRead)
119+ val totalToTransfer = fileSizeInBytes ? : 0L
120+ var totalBytesRead = 0L
121+
122+ BufferedInputStream (responseStream).use { bis ->
123+ outputStream.use { fos ->
124+ val buffer = ByteArray (BUFFER_SIZE )
125+ var bytesRead: Int
126+ while (bis.read(buffer).also { bytesRead = it } != - 1 ) {
127+ if (cancellationRequested.get()) throw OperationCancelledException ()
128+ fos.write(buffer, 0 , bytesRead)
129+ totalBytesRead + = bytesRead
130+ dataTransferListeners.forEach { listener ->
131+ listener.onTransferProgress(
132+ bytesRead.toLong(),
133+ totalBytesRead,
134+ totalToTransfer,
135+ targetPath.toString()
136+ )
137+ }
138+ }
108139 }
109140 }
110141 }
111- }
112142
113- private fun readMetadata (getMethod : GetMethod ) {
114- val modificationTime =
115- getMethod.getResponseHeader(" Last-Modified" )
116- ? : getMethod.getResponseHeader(" last-modified" )
143+ private fun readMetadata (getMethod : GetMethod ) {
144+ val modificationTime =
145+ getMethod.getResponseHeader(" Last-Modified" )
146+ ? : getMethod.getResponseHeader(" last-modified" )
117147
118- if (modificationTime != null ) {
119- modificationTimestamp = WebdavUtils .parseResponseDate(modificationTime)?.time ? : 0
120- } else {
121- Log_OC .e(TAG , " Could not read modification time from response downloading $remotePath " )
122- }
148+ if (modificationTime != null ) {
149+ modificationTimestamp = WebdavUtils .parseResponseDate(modificationTime)?.time ? : 0
150+ } else {
151+ Log_OC .e(TAG , " Could not read modification time from response downloading $remotePath " )
152+ }
123153
124- etag = WebdavUtils .getEtagFromResponse(getMethod)
125- if (etag.isEmpty()) {
126- Log_OC .e(TAG , " Could not read eTag from response downloading $remotePath " )
154+ etag = WebdavUtils .getEtagFromResponse(getMethod)
155+ if (etag.isEmpty()) {
156+ Log_OC .e(TAG , " Could not read eTag from response downloading $remotePath " )
157+ }
127158 }
128- }
129159
130- private fun isSuccess (status : Int ) = (status == HttpStatus .SC_OK )
160+ private fun isSuccess (status : Int ) = (status == HttpStatus .SC_OK )
131161
132- private val tmpPath: String
133- get() = temporalFolderPath + remotePath
134- // endregion
162+ private val tmpPath: String
163+ get() = temporalFolderPath + remotePath
164+ // endregion
135165
136- // region public methods
137- fun addProgressListener (listener : OnDatatransferProgressListener ) {
138- dataTransferListeners.add(listener)
139- }
166+ // region public methods
167+ fun addProgressListener (listener : OnDatatransferProgressListener ) {
168+ dataTransferListeners.add(listener)
169+ }
140170
141- fun removeProgressListener (listener : OnDatatransferProgressListener ) {
142- dataTransferListeners.remove(listener)
143- }
171+ fun removeProgressListener (listener : OnDatatransferProgressListener ) {
172+ dataTransferListeners.remove(listener)
173+ }
144174
145- fun cancel () {
146- cancellationRequested.set(true )
147- }
148- // endregion
175+ fun cancel () {
176+ cancellationRequested.set(true )
177+ }
178+ // endregion
179+
180+ companion object {
181+ private val TAG = DownloadFileRemoteOperation ::class .java.simpleName
182+ private const val BUFFER_SIZE = 4096
149183
150- companion object {
151- private val TAG = DownloadFileRemoteOperation ::class .java.simpleName
152- private const val BUFFER_SIZE = 4096
184+ private const val BYTES_PER_GB_LONG = 1_000_000_000L
185+ private const val READ_TIMEOUT_MIN = 60 * 1000L // 1 min
186+ private const val READ_TIMEOUT_PER_GB = 3 * 60 * 1000L // 3 min per GB
187+ private const val READ_TIMEOUT_MAX = 30 * 60 * 1000L // 30 min cap
188+ private const val CONNECTION_TIMEOUT = 15 * 1000 // 15 s
189+ }
153190 }
154- }
0 commit comments