@@ -26,16 +26,22 @@ import androidx.work.OneTimeWorkRequest
2626import androidx.work.WorkManager
2727import androidx.work.Worker
2828import androidx.work.WorkerParameters
29+ import at.bitfire.dav4jvm.DavResource
2930import autodagger.AutoInjector
3031import com.nextcloud.talk.R
3132import com.nextcloud.talk.activities.MainActivity
3233import com.nextcloud.talk.api.NcApi
34+ import com.nextcloud.talk.api.NcApiCoroutines
3335import com.nextcloud.talk.application.NextcloudTalkApplication
36+ import com.nextcloud.talk.dagger.modules.RestModule
3437import com.nextcloud.talk.data.user.model.User
38+ import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ChatProbeAttachmentData
39+ import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ProbeConversationAttachmentRequest
3540import com.nextcloud.talk.upload.chunked.ChunkedFileUploader
3641import com.nextcloud.talk.upload.chunked.OnDataTransferProgressListener
3742import com.nextcloud.talk.upload.normal.FileUploader
3843import com.nextcloud.talk.users.UserManager
44+ import com.nextcloud.talk.utils.ApiUtils
3945import com.nextcloud.talk.utils.CapabilitiesUtil
4046import com.nextcloud.talk.utils.FileUtils
4147import com.nextcloud.talk.utils.NotificationUtils
@@ -45,9 +51,16 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
4551import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
4652import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
4753import com.nextcloud.talk.utils.preferences.AppPreferences
54+ import kotlinx.coroutines.runBlocking
55+ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
4856import okhttp3.MediaType.Companion.toMediaTypeOrNull
4957import okhttp3.OkHttpClient
58+ import okhttp3.Protocol
59+ import okhttp3.RequestBody
60+ import okhttp3.Response
5061import java.io.File
62+ import java.io.IOException
63+ import java.util.UUID
5164import javax.inject.Inject
5265
5366@AutoInjector(NextcloudTalkApplication ::class )
@@ -61,6 +74,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
6174 @Inject
6275 lateinit var userManager: UserManager
6376
77+ @Inject
78+ lateinit var ncApiCoroutines: NcApiCoroutines
79+
6480 @Inject
6581 lateinit var currentUserProvider: CurrentUserProviderOld
6682
@@ -107,9 +123,12 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
107123 file = FileUtils .getFileFromUri(context, sourceFileUri)
108124 val remotePath = getRemotePath(currentUser)
109125
126+ val useConversationSubfolders = CapabilitiesUtil .hasConversationSubfoldersForAttachments(
127+ currentUser.capabilities!! .spreedCapability!!
128+ )
110129 initNotificationSetup()
111130 file?.let { isChunkedUploading = it.length() > CHUNK_UPLOAD_THRESHOLD_SIZE }
112- val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath)
131+ val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath, useConversationSubfolders )
113132
114133 if (uploadSuccess) {
115134 cancelNotification()
@@ -129,25 +148,138 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
129148 }
130149 }
131150
132- private fun uploadFile (sourceFileUri : Uri , metaData : String? , remotePath : String ): Boolean =
151+ private fun uploadFile (
152+ sourceFileUri : Uri ,
153+ metaData : String? ,
154+ remotePath : String ,
155+ useConversationSubfolders : Boolean
156+ ): Boolean =
133157 if (file == null ) {
134158 false
159+ } else if (useConversationSubfolders) {
160+ uploadUsingConversationSubfolders(sourceFileUri, metaData)
135161 } else if (isChunkedUploading) {
136162 Log .d(TAG , " starting chunked upload because size is " + file!! .length())
137-
138163 initNotificationWithPercentage()
139164 val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
140-
141- chunkedFileUploader = ChunkedFileUploader (okHttpClient, currentUser, roomToken, metaData, this )
165+ chunkedFileUploader = ChunkedFileUploader (
166+ okHttpClient,
167+ currentUser,
168+ roomToken,
169+ metaData,
170+ this ,
171+ ncApiCoroutines,
172+ useConversationSubfolders
173+ )
142174 chunkedFileUploader!! .upload(file!! , mimeType, remotePath)
143175 } else {
144176 Log .d(TAG , " starting normal upload (not chunked) of $fileName " )
145-
146177 FileUploader (okHttpClient, context, currentUser, roomToken, ncApi, file!! )
147178 .upload(sourceFileUri, fileName, remotePath, metaData)
148179 .blockingFirst()
149180 }
150181
182+ private fun uploadUsingConversationSubfolders (sourceFileUri : Uri , metaData : String? ): Boolean =
183+ runBlocking {
184+ val credentials = ApiUtils .getCredentials(
185+ currentUser.username,
186+ currentUser.token
187+ ) ? : return @runBlocking false
188+ val uploadId = UUID .randomUUID().toString()
189+ val request = ProbeConversationAttachmentRequest ().apply {
190+ fileNames = listOf (fileName)
191+ }
192+ val probeResponse = ncApiCoroutines.probeConversationAttachmentFolder(
193+ credentials,
194+ ApiUtils .getUrlForChatAttachmentFolder(ApiUtils .API_V1 , currentUser.baseUrl, roomToken),
195+ request)
196+
197+ val draftFolderPath = probeResponse.ocs?.data?.folder?.trimEnd(' /' )
198+ if (draftFolderPath.isNullOrEmpty()) {
199+ Log .e(TAG , " Draft folder path missing in probe response" )
200+ return @runBlocking false
201+ }
202+ val predictedName = resolveFinalFileName(fileName, probeResponse.ocs?.data!! )
203+ val tempRemotePath = " $draftFolderPath /$uploadId -0-$fileName "
204+
205+ val uploadSuccess = if (isChunkedUploading) {
206+ initNotificationWithPercentage()
207+ val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
208+ chunkedFileUploader = ChunkedFileUploader (
209+ okHttpClient,
210+ currentUser,
211+ roomToken,
212+ metaData,
213+ this @UploadAndShareFilesWorker,
214+ ncApiCoroutines,
215+ true
216+ )
217+ chunkedFileUploader!! .upload(file!! , mimeType, tempRemotePath)
218+ } else {
219+ uploadFileToDraftPathUsingWebDav(sourceFileUri, tempRemotePath)
220+ }
221+
222+ if (! uploadSuccess) {
223+ return @runBlocking false
224+ }
225+
226+ runCatching {
227+ ncApiCoroutines.postConversationAttachment(
228+ credentials,
229+ ApiUtils .getUrlForChatAttachment(ApiUtils .API_V1 , currentUser.baseUrl, roomToken),
230+ tempRemotePath,
231+ uploadId,
232+ predictedName,
233+ metaData
234+ )
235+ }
236+ .onFailure { Log .e(TAG , " Failed to finalize uploaded attachment" , it) }
237+ .isSuccess
238+ }
239+
240+ private fun resolveFinalFileName (originalName : String , probeData : ChatProbeAttachmentData ): String =
241+ probeData.renames?.get(originalName) ? : originalName
242+
243+ private fun uploadFileToDraftPathUsingWebDav (sourceFileUri : Uri , remotePath : String ): Boolean =
244+ runCatching {
245+ val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
246+ val requestBody = RequestBody .create(mimeType, file!! )
247+ val uploadUrl = ApiUtils .getUrlForFileUpload(
248+ currentUser.baseUrl!! ,
249+ currentUser.userId!! ,
250+ remotePath
251+ )
252+ val davResource = DavResource (
253+ createWebDavClientWithAuth(),
254+ uploadUrl.toHttpUrlOrNull()!!
255+ )
256+ davResource.put(requestBody) { response: Response ->
257+ if (! response.isSuccessful) {
258+ throw IOException (" Failed to upload file to draft folder. response code: ${response.code} " )
259+ }
260+ }
261+ FileUtils .copyFileToCache(context, sourceFileUri, fileName)
262+ true
263+ }
264+ .onFailure { Log .e(TAG , " Failed to upload draft attachment via WebDAV" , it) }
265+ .getOrDefault(false )
266+
267+ private fun createWebDavClientWithAuth (): OkHttpClient =
268+ okHttpClient.newBuilder()
269+ .followRedirects(false )
270+ .followSslRedirects(false )
271+ .protocols(listOf (Protocol .HTTP_1_1 ))
272+ .authenticator(
273+ RestModule .HttpAuthenticator (
274+ ApiUtils .getCredentials(
275+ currentUser.username,
276+ currentUser.token
277+ )!! ,
278+ " Authorization"
279+ )
280+ )
281+ .build()
282+
151283 private fun getRemotePath (currentUser : User ): String {
152284 val remotePath = CapabilitiesUtil .getAttachmentFolder(
153285 currentUser.capabilities!! .spreedCapability!!
0 commit comments