From 2403eb3857e1ec259ee89471879c4418247d1ef5 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Tue, 1 Dec 2020 17:19:48 -0300 Subject: [PATCH] [FIX] Filenames are incorrect in non-latin alphabets on upload (#2671) * fix: filename on react-native-image-crop-picker * fix: use rn-fetch-blob to upload files * fix: FileUpload as a service * fix: cancel upload on iOS * fix: file upload from share extension Co-authored-by: Diego Mello --- app/lib/methods/sendFileMessage.js | 63 ++++++++++--------- app/utils/fileUpload/index.android.js | 22 +++++++ app/utils/fileUpload/index.ios.js | 48 ++++++++++++++ app/views/ShareListView/index.js | 2 +- ...eact-native-image-crop-picker+0.31.1.patch | 12 ++++ 5 files changed, 117 insertions(+), 30 deletions(-) create mode 100644 app/utils/fileUpload/index.android.js create mode 100644 app/utils/fileUpload/index.ios.js create mode 100644 patches/react-native-image-crop-picker+0.31.1.patch diff --git a/app/lib/methods/sendFileMessage.js b/app/lib/methods/sendFileMessage.js index 041b1f40..923da172 100644 --- a/app/lib/methods/sendFileMessage.js +++ b/app/lib/methods/sendFileMessage.js @@ -1,6 +1,7 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { settings as RocketChatSettings } from '@rocket.chat/sdk'; +import FileUpload from '../../utils/fileUpload'; import database from '../database'; import log from '../../utils/log'; @@ -12,7 +13,11 @@ export function isUploadActive(path) { export async function cancelUpload(item) { if (uploadQueue[item.path]) { - uploadQueue[item.path].abort(); + try { + await uploadQueue[item.path].cancel(); + } catch { + // Do nothing + } try { const db = database.active; await db.action(async() => { @@ -32,9 +37,6 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { const uploadUrl = `${ server }/api/v1/rooms.upload/${ rid }`; - const xhr = new XMLHttpRequest(); - const formData = new FormData(); - fileInfo.rid = rid; const db = database.active; @@ -56,31 +58,38 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { } } - uploadQueue[fileInfo.path] = xhr; - xhr.open('POST', uploadUrl); - - formData.append('file', { - uri: fileInfo.path, + const formData = []; + formData.push({ + name: 'file', type: fileInfo.type, - name: encodeURI(fileInfo.name) || 'fileMessage' + filename: fileInfo.name || 'fileMessage', + uri: fileInfo.path }); if (fileInfo.description) { - formData.append('description', fileInfo.description); + formData.push({ + name: 'description', + data: fileInfo.description + }); } if (tmid) { - formData.append('tmid', tmid); + formData.push({ + name: 'tmid', + data: tmid + }); } - xhr.setRequestHeader('X-Auth-Token', token); - xhr.setRequestHeader('X-User-Id', id); - const { customHeaders } = RocketChatSettings; - Object.keys(customHeaders).forEach((key) => { - xhr.setRequestHeader(key, customHeaders[key]); - }); + const headers = { + ...RocketChatSettings.customHeaders, + 'Content-Type': 'multipart/form-data', + 'X-Auth-Token': token, + 'X-User-Id': id + }; - xhr.upload.onprogress = async({ total, loaded }) => { + uploadQueue[fileInfo.path] = FileUpload.fetch('POST', uploadUrl, headers, formData); + + uploadQueue[fileInfo.path].uploadProgress(async(loaded, total) => { try { await db.action(async() => { await uploadRecord.update((u) => { @@ -90,15 +99,14 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { } catch (e) { log(e); } - }; + }); - xhr.onload = async() => { - if (xhr.status >= 200 && xhr.status < 400) { // If response is all good... + uploadQueue[fileInfo.path].then(async(response) => { + if (response.respInfo.status >= 200 && response.respInfo.status < 400) { // If response is all good... try { await db.action(async() => { await uploadRecord.destroyPermanently(); }); - const response = JSON.parse(xhr.response); resolve(response); } catch (e) { log(e); @@ -114,15 +122,14 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { log(e); } try { - const response = JSON.parse(xhr.response); reject(response); } catch (e) { reject(e); } } - }; + }); - xhr.onerror = async(error) => { + uploadQueue[fileInfo.path].catch(async(error) => { try { await db.action(async() => { await uploadRecord.update((u) => { @@ -133,9 +140,7 @@ export function sendFileMessage(rid, fileInfo, tmid, server, user) { log(e); } reject(error); - }; - - xhr.send(formData); + }); } catch (e) { log(e); } diff --git a/app/utils/fileUpload/index.android.js b/app/utils/fileUpload/index.android.js new file mode 100644 index 00000000..6cf45246 --- /dev/null +++ b/app/utils/fileUpload/index.android.js @@ -0,0 +1,22 @@ +import RNFetchBlob from 'rn-fetch-blob'; + +class FileUpload { + fetch = (method, url, headers, data) => { + const formData = data.map((item) => { + if (item.uri) { + return { + name: item.name, + type: item.type, + filename: item.filename, + data: RNFetchBlob.wrap(decodeURI(item.uri)) + }; + } + return item; + }); + + return RNFetchBlob.fetch(method, url, headers, formData); + } +} + +const fileUpload = new FileUpload(); +export default fileUpload; diff --git a/app/utils/fileUpload/index.ios.js b/app/utils/fileUpload/index.ios.js new file mode 100644 index 00000000..81a94d87 --- /dev/null +++ b/app/utils/fileUpload/index.ios.js @@ -0,0 +1,48 @@ +class FileUpload { + _xhr = new XMLHttpRequest(); + + _formData = new FormData(); + + fetch = (method, url, headers, data) => { + this._xhr.open(method, url); + + Object.keys(headers).forEach((key) => { + this._xhr.setRequestHeader(key, headers[key]); + }); + + data.forEach((item) => { + if (item.uri) { + this._formData.append(item.name, { + uri: item.uri, + type: item.type, + name: item.filename + }); + } else { + this._formData.append(item.name, item.data); + } + }); + + return this; + } + + then = (callback) => { + this._xhr.onload = () => callback({ respInfo: this._xhr }); + this._xhr.send(this._formData); + } + + catch = (callback) => { + this._xhr.onerror = callback; + } + + uploadProgress = (callback) => { + this._xhr.upload.onprogress = ({ total, loaded }) => callback(loaded, total); + } + + cancel = () => { + this._xhr.abort(); + return Promise.resolve(); + } +} + +const fileUpload = new FileUpload(); +export default fileUpload; diff --git a/app/views/ShareListView/index.js b/app/views/ShareListView/index.js index 6bcf295d..f2b1d25c 100644 --- a/app/views/ShareListView/index.js +++ b/app/views/ShareListView/index.js @@ -74,7 +74,7 @@ class ShareListView extends React.Component { } const info = await Promise.all(data.filter(item => item.type === 'media').map(file => FileSystem.getInfoAsync(this.uriToPath(file.value), { size: true }))); const attachments = info.map(file => ({ - filename: file.uri.substring(file.uri.lastIndexOf('/') + 1), + filename: decodeURIComponent(file.uri.substring(file.uri.lastIndexOf('/') + 1)), description: '', size: file.size, mime: mime.lookup(file.uri), diff --git a/patches/react-native-image-crop-picker+0.31.1.patch b/patches/react-native-image-crop-picker+0.31.1.patch new file mode 100644 index 00000000..167e876e --- /dev/null +++ b/patches/react-native-image-crop-picker+0.31.1.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java +index 3500542..94e45b6 100644 +--- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java ++++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java +@@ -584,6 +584,7 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi + image.putInt("height", options.outHeight); + image.putString("mime", options.outMimeType); + image.putInt("size", (int) new File(compressedImagePath).length()); ++ image.putString("filename", compressedImage.getName()); + image.putString("modificationDate", String.valueOf(modificationDate)); + + if (includeBase64) {