@@ -26,16 +26,23 @@ 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.chatpostattachment.PostConversationAttachmentResponse
39+ import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ChatProbeAttachmentData
40+ import com.nextcloud.talk.models.json.chatprobeattachmentfolder.ProbeConversationAttachmentRequest
3541import com.nextcloud.talk.upload.chunked.ChunkedFileUploader
3642import com.nextcloud.talk.upload.chunked.OnDataTransferProgressListener
3743import com.nextcloud.talk.upload.normal.FileUploader
3844import com.nextcloud.talk.users.UserManager
45+ import com.nextcloud.talk.utils.ApiUtils
3946import com.nextcloud.talk.utils.CapabilitiesUtil
4047import com.nextcloud.talk.utils.FileUtils
4148import com.nextcloud.talk.utils.NotificationUtils
@@ -45,9 +52,16 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
4552import com.nextcloud.talk.utils.database.user.CurrentUserProviderOld
4653import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
4754import com.nextcloud.talk.utils.preferences.AppPreferences
55+ import kotlinx.coroutines.runBlocking
56+ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
4857import okhttp3.MediaType.Companion.toMediaTypeOrNull
4958import okhttp3.OkHttpClient
59+ import okhttp3.Protocol
60+ import okhttp3.RequestBody
61+ import okhttp3.Response
5062import java.io.File
63+ import java.io.IOException
64+ import java.util.UUID
5165import javax.inject.Inject
5266
5367@AutoInjector(NextcloudTalkApplication ::class )
@@ -61,6 +75,9 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
6175 @Inject
6276 lateinit var userManager: UserManager
6377
78+ @Inject
79+ lateinit var ncApiCoroutines: NcApiCoroutines
80+
6481 @Inject
6582 lateinit var currentUserProvider: CurrentUserProviderOld
6683
@@ -107,9 +124,12 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
107124 file = FileUtils .getFileFromUri(context, sourceFileUri)
108125 val remotePath = getRemotePath(currentUser)
109126
127+ val useConversationSubfolders = CapabilitiesUtil .hasConversationSubfoldersForAttachments(
128+ currentUser.capabilities!! .spreedCapability!!
129+ )
110130 initNotificationSetup()
111131 file?.let { isChunkedUploading = it.length() > CHUNK_UPLOAD_THRESHOLD_SIZE }
112- val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath)
132+ val uploadSuccess: Boolean = uploadFile(sourceFileUri, metaData, remotePath, useConversationSubfolders )
113133
114134 if (uploadSuccess) {
115135 cancelNotification()
@@ -129,25 +149,154 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
129149 }
130150 }
131151
132- private fun uploadFile (sourceFileUri : Uri , metaData : String? , remotePath : String ): Boolean =
152+ private fun uploadFile (
153+ sourceFileUri : Uri ,
154+ metaData : String? ,
155+ remotePath : String ,
156+ useConversationSubfolders : Boolean
157+ ): Boolean =
133158 if (file == null ) {
134159 false
160+ } else if (useConversationSubfolders) {
161+ uploadUsingConversationSubfolders(sourceFileUri, metaData)
135162 } else if (isChunkedUploading) {
136163 Log .d(TAG , " starting chunked upload because size is " + file!! .length())
137-
138164 initNotificationWithPercentage()
139165 val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
140-
141- chunkedFileUploader = ChunkedFileUploader (okHttpClient, currentUser, roomToken, metaData, this )
166+ chunkedFileUploader = ChunkedFileUploader (
167+ okHttpClient,
168+ currentUser,
169+ roomToken,
170+ metaData,
171+ this ,
172+ ncApiCoroutines,
173+ useConversationSubfolders
174+ )
142175 chunkedFileUploader!! .upload(file!! , mimeType, remotePath)
143176 } else {
144177 Log .d(TAG , " starting normal upload (not chunked) of $fileName " )
145-
146- FileUploader (okHttpClient, context, currentUser, roomToken, ncApi, file!! )
178+ FileUploader (
179+ okHttpClient,
180+ context,
181+ currentUser,
182+ roomToken,
183+ ncApi,
184+ file!! ,
185+ ncApiCoroutines,
186+ useConversationSubfolders
187+ )
147188 .upload(sourceFileUri, fileName, remotePath, metaData)
148189 .blockingFirst()
149190 }
150191
192+ private fun uploadUsingConversationSubfolders (sourceFileUri : Uri , metaData : String? ): Boolean =
193+ runBlocking {
194+ val credentials = ApiUtils .getCredentials(
195+ currentUser.username,
196+ currentUser.token
197+ ) ? : return @runBlocking false
198+ val uploadId = UUID .randomUUID().toString()
199+ val fileNames = ProbeConversationAttachmentRequest ().apply {
200+ fileNames = listOf (fileName)
201+ }
202+
203+ val probeResponse = ncApiCoroutines.probeConversationAttachmentFolder(
204+ credentials,
205+ ApiUtils .getUrlForChatAttachmentFolder(ApiUtils .API_V1 , currentUser.baseUrl, roomToken),
206+ fileNames
207+ )
208+
209+ val draftFolderPath = probeResponse.ocs?.data?.folder
210+ if (draftFolderPath.isNullOrEmpty()) {
211+ Log .e(TAG , " Draft folder path missing in probe response" )
212+ return @runBlocking false
213+ }
214+ val predictedName = resolveFinalFileName(fileName, probeResponse.ocs?.data!! )
215+ val tempRemotePath = " /$draftFolderPath /$uploadId -$fileName "
216+
217+ val uploadSuccess = if (isChunkedUploading) {
218+ initNotificationWithPercentage()
219+ val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
220+ chunkedFileUploader = ChunkedFileUploader (
221+ okHttpClient,
222+ currentUser,
223+ roomToken,
224+ metaData,
225+ this @UploadAndShareFilesWorker,
226+ ncApiCoroutines,
227+ true
228+ )
229+ chunkedFileUploader!! .upload(file!! , mimeType, tempRemotePath)
230+ } else {
231+ FileUploader (okHttpClient, context, currentUser, roomToken, ncApi, file!! , ncApiCoroutines, true )
232+ .uploadToConversationSubfolder(sourceFileUri, tempRemotePath)
233+ }
234+
235+ if (! uploadSuccess) {
236+ return @runBlocking false
237+ }
238+
239+ val params = PostConversationAttachmentResponse ().apply {
240+ filePath = tempRemotePath
241+ referenceId = uploadId
242+ talkMetaData = metaData
243+ fileName = predictedName
244+ }
245+
246+ runCatching {
247+ ncApiCoroutines.postConversationAttachment(
248+ credentials,
249+ ApiUtils .getUrlForChatAttachment(ApiUtils .API_V1 , currentUser.baseUrl, roomToken),
250+ params
251+ )
252+ }
253+ .onFailure { Log .e(TAG , " Failed to finalize uploaded attachment" , it) }
254+ .isSuccess
255+ }
256+
257+ private fun resolveFinalFileName (originalName : String , probeData : ChatProbeAttachmentData ): String =
258+ probeData.renames?.get(originalName) ? : originalName
259+
260+ private fun uploadFileToDraftPathUsingWebDav (sourceFileUri : Uri , remotePath : String ): Boolean =
261+ runCatching {
262+ val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
263+ val requestBody = RequestBody .create(mimeType, file!! )
264+ val uploadUrl = ApiUtils .getUrlForFileUpload(
265+ currentUser.baseUrl!! ,
266+ currentUser.userId!! ,
267+ remotePath
268+ )
269+ val davResource = DavResource (
270+ createWebDavClientWithAuth(),
271+ uploadUrl.toHttpUrlOrNull()!!
272+ )
273+ davResource.put(requestBody) { response: Response ->
274+ if (! response.isSuccessful) {
275+ throw IOException (" Failed to upload file to draft folder. response code: ${response.code} " )
276+ }
277+ }
278+ FileUtils .copyFileToCache(context, sourceFileUri, fileName)
279+ true
280+ }
281+ .onFailure { Log .e(TAG , " Failed to upload draft attachment via WebDAV" , it) }
282+ .getOrDefault(false )
283+
284+ private fun createWebDavClientWithAuth (): OkHttpClient =
285+ okHttpClient.newBuilder()
286+ .followRedirects(false )
287+ .followSslRedirects(false )
288+ .protocols(listOf (Protocol .HTTP_1_1 ))
289+ .authenticator(
290+ RestModule .HttpAuthenticator (
291+ ApiUtils .getCredentials(
292+ currentUser.username,
293+ currentUser.token
294+ )!! ,
295+ " Authorization"
296+ )
297+ )
298+ .build()
299+
151300 private fun getRemotePath (currentUser : User ): String {
152301 val remotePath = CapabilitiesUtil .getAttachmentFolder(
153302 currentUser.capabilities!! .spreedCapability!!
0 commit comments