mobile: improve file uploads in background

This commit is contained in:
Ammar Ahmed
2025-11-28 14:34:18 +05:00
parent 11e7d34961
commit bcfff70e78
7 changed files with 111 additions and 45 deletions

View File

@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { RequestOptions } from "@notesnook/core";
import { Platform } from "react-native";
import { isImage, RequestOptions } from "@notesnook/core";
import { PermissionsAndroid, Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
import { ToastManager } from "../../services/event-manager";
import { useAttachmentStore } from "../../stores/use-attachment-store";
@@ -31,6 +31,7 @@ import {
FileSizeResult,
getUploadedFileSize
} from "./utils";
import Upload from "@ammarahmed/react-native-upload";
export async function uploadFile(
filename: string,
@@ -64,47 +65,87 @@ export async function uploadFile(
);
}
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
const fileInfo = await RNFetchBlob.fs.stat(filePath);
const remoteFileSize = await getUploadedFileSize(filename);
if (remoteFileSize === FileSizeResult.Error) return false;
if (remoteFileSize > FileSizeResult.Empty && remoteFileSize === fileSize) {
if (
remoteFileSize > FileSizeResult.Empty &&
remoteFileSize === fileInfo.size
) {
DatabaseLogger.log(`File ${filename} is already uploaded.`);
return true;
}
DatabaseLogger.info(`Starting upload: ${filename}`);
let attachmentInfo = await db.attachments.attachment(filename);
const uploadRequest = RNFetchBlob.config({
//@ts-ignore
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
})
.fetch(
"PUT",
url,
{
...headers,
"content-type": ""
},
RNFetchBlob.wrap(filePath)
)
.uploadProgress((sent, total) => {
useAttachmentStore
.getState()
.setProgress(sent, total, filename, 0, "upload");
DatabaseLogger.info(
`File upload progress: ${filename}, ${sent}/${total}`
);
});
DatabaseLogger.info(
`Starting upload of ${filename} at path: ${fileInfo.path} ${fileInfo.size}`
);
if (Platform.OS === "android") {
const status = await PermissionsAndroid.request(
"android.permission.POST_NOTIFICATIONS"
);
if (status !== "granted") {
ToastManager.show({
message: `The permission to show file upload notification was disallowed by the user.`,
type: "info"
});
}
}
const upload = Upload.create({
customUploadId: filename,
path: Platform.OS === "ios" ? "file://" + fileInfo.path : fileInfo.path,
url: url,
method: "PUT",
headers: {
...headers,
"content-type": "application/octet-stream"
},
appGroup: IOS_APPGROUPID,
notification: {
filename:
attachmentInfo && isImage(attachmentInfo?.mimeType)
? "image"
: attachmentInfo?.filename || "file",
enabled: true,
enableRingTone: true,
autoClear: true
}
}).onChange((event) => {
switch (event.status) {
case "running":
case "pending":
useAttachmentStore
.getState()
.setProgress(
event.uploadedBytes || 0,
event.totalBytes || fileInfo.size,
filename,
0,
"upload"
);
DatabaseLogger.info(
`File upload progress: ${filename}, ${event.uploadedBytes}/${
event.totalBytes || fileInfo.size
}`
);
break;
case "completed":
console.log("Upload completed");
break;
}
});
const result = await upload.start();
cancelToken.cancel = async () => {
useAttachmentStore.getState().remove(filename);
uploadRequest.cancel();
upload.cancel();
};
const uploadResponse = await uploadRequest;
const status = uploadResponse.info().status;
const status = result.responseCode || 0;
const uploaded = status >= 200 && status < 300;
useAttachmentStore.getState().remove(filename);
@@ -114,12 +155,12 @@ export async function uploadFile(
throw new Error(
`${status}, name: ${fileInfo.filename}, length: ${
fileInfo.size
}, info: ${JSON.stringify(uploadResponse.info())}`
}, info: ${JSON.stringify(result.error)}`
);
}
const attachment = await db.attachments.attachment(filename);
if (!attachment) return false;
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
attachmentInfo = await db.attachments.attachment(filename);
if (!attachmentInfo) return false;
await checkUpload(filename, requestOptions.chunkSize, attachmentInfo.size);
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
return uploaded;
} catch (e) {

View File

@@ -205,7 +205,9 @@ const camera = async (options: PickerOptions) => {
options
);
})
.catch((e) => {});
.catch((e) => {
console.log(e);
});
} catch (e) {
ToastManager.show({
heading: (e as Error).message,

View File

@@ -1104,7 +1104,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.10-beta.1;
MARKETING_VERSION = "3.3.10-beta.1";
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -1210,7 +1210,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 3.3.10-beta.1;
MARKETING_VERSION = "3.3.10-beta.1";
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -1379,7 +1379,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.10-beta.1;
MARKETING_VERSION = "3.3.10-beta.1";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
@@ -1423,7 +1423,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 3.3.10-beta.1;
MARKETING_VERSION = "3.3.10-beta.1";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.notewidget;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1534,7 +1534,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.10-beta.1;
MARKETING_VERSION = "3.3.10-beta.1";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
@@ -1647,7 +1647,7 @@
"@executable_path/../../Frameworks",
);
LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift$(inherited)";
MARKETING_VERSION = 3.3.10-beta.1;
MARKETING_VERSION = "3.3.10-beta.1";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = org.streetwriters.notesnook.share;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -4,6 +4,7 @@
#import "RNShortcuts.h"
#import "RNBootSplash.h"
#import <ReactAppDependencyProvider/RCTAppDependencyProvider.h>
#import "RNFileUploader.h"
@interface ReactNativeDelegate : RCTDefaultReactNativeFactoryDelegate
@end
@@ -79,4 +80,8 @@
[RNBootSplash initWithStoryboard:@"BootSplash" rootView:rootView]; // ⬅️ initialize the splash screen
}
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
[RNFileUploader setCompletionHandlerWithIdentifier:identifier completionHandler:completionHandler];
}
@end

View File

@@ -1887,7 +1887,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-document-picker (10.1.7):
- react-native-document-picker (11.0.1):
- boost
- DoubleConversion
- fast_float
@@ -1969,14 +1969,14 @@ PODS:
- React-Core
- React-RCTFabric
- ReactCommon/turbomodule/core
- react-native-mmkv-storage (0.11.2):
- react-native-mmkv-storage (12.0.0):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- MMKV (~> 1.3.9)
- MMKV (~> 1.3.14)
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
@@ -2244,6 +2244,8 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-upload (6.27.0):
- React
- react-native-webview (13.16.0):
- boost
- DoubleConversion
@@ -3462,6 +3464,7 @@ DEPENDENCIES:
- "react-native-share-extension (from `../node_modules/@ammarahmed/react-native-share-extension`)"
- "react-native-sodium (from `../node_modules/@ammarahmed/react-native-sodium`)"
- react-native-theme-switch-animation (from `../node_modules/react-native-theme-switch-animation`)
- "react-native-upload (from `../node_modules/@ammarahmed/react-native-upload`)"
- react-native-webview (from `../node_modules/react-native-webview`)
- React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`)
@@ -3675,6 +3678,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@ammarahmed/react-native-sodium"
react-native-theme-switch-animation:
:path: "../node_modules/react-native-theme-switch-animation"
react-native-upload:
:path: "../node_modules/@ammarahmed/react-native-upload"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-NativeModulesApple:
@@ -3855,7 +3860,7 @@ SPEC CHECKSUMS:
react-native-blob-util: 7946b7e13acf0da5e849dc2f73fcfebe1d981699
react-native-config: 963b5efabc864cf69412e54b5de49b6a23e4af03
react-native-date-picker: 4f4f40f6e65798038bb4b1bff47890c2be69c2e6
react-native-document-picker: 1734eb0aa3dbd1cd7bf1b105936f9b55031ae616
react-native-document-picker: 254467fec90f263dfc4828210daf3e8baa4fcb81
react-native-fingerprint-scanner: d5e143a361f3f01858e9c45141ddcabc4fd57055
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
react-native-gzip: 794e0e964a0d9e1dfd1773fee938adb4d4310e26
@@ -3863,7 +3868,7 @@ SPEC CHECKSUMS:
react-native-image-resizer: 290b045c34c69db7574e4d08aadfc4abe1ff5a99
react-native-in-app-review: 1516ba69d60d58053b7eb3aaaf8d2a5a74af8b57
react-native-keep-awake: a351e6f67006b47f316ae2b17ee8ee69386167f4
react-native-mmkv-storage: b7ce5a5eb3d3bfddcf2df127671abba6ef3d76ae
react-native-mmkv-storage: 31b7b155e690339f5a25cfe56d5e2f68dee4f066
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-notification-sounds: ce106d58df0dd384bccbd2e84fb53accab7cc068
react-native-orientation-locker: cc6f357b289a2e0dd2210fea0c52cb8e0727fdaa
@@ -3875,6 +3880,7 @@ SPEC CHECKSUMS:
react-native-share-extension: fdc6aaab51591a2d445df239c446aaa3a99658ec
react-native-sodium: 066f76e46c9be13e9260521e3fa994937c4cdab4
react-native-theme-switch-animation: 449d6db7a760f55740505e7403ae8061debc9a7e
react-native-upload: f0198bb4db00ff7dae14904ad59da470550be075
react-native-webview: 654f794a7686b47491cf43aa67f7f428bea00eed
React-NativeModulesApple: 46690a0fe94ec28fc6fc686ec797b911d251ded0
React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb

View File

@@ -16,6 +16,7 @@
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
"@ammarahmed/react-native-share-extension": "^2.9.5",
"@ammarahmed/react-native-sodium": "^1.6.8",
"@ammarahmed/react-native-upload": "^6.27.0",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bam.tech/react-native-image-resizer": "3.0.11",
"@callstack/repack": "~5.2.1",
@@ -529,6 +530,16 @@
"integrity": "sha512-FbX9fMfqJ3ysd+zkSS5di459tPv5iB0fhmK3dx/xo+l07sf7f1gzJu17mghSGkOKkbjXaoLPcq3XLMwWGpvapQ==",
"license": "ISC"
},
"node_modules/@ammarahmed/react-native-upload": {
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/@ammarahmed/react-native-upload/-/react-native-upload-6.27.0.tgz",
"integrity": "sha512-RCTK1dbaLjCr4tb/XMIFCE+fxZzx3yOUyyGMs8wzj28f10xzYEFrl9fYYDgbHkf5TcXu3zCJBetafllaKBc/ug==",
"license": "BSD-3-Clause",
"peerDependencies": {
"react": "*",
"react-native": ">=0.47.0"
}
},
"node_modules/@azure/core-asynciterator-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz",

View File

@@ -32,6 +32,7 @@
"@ammarahmed/react-native-fingerprint-scanner": "^5.0.1",
"@ammarahmed/react-native-share-extension": "^2.9.5",
"@ammarahmed/react-native-sodium": "^1.6.8",
"@ammarahmed/react-native-upload": "^6.27.0",
"@azure/core-asynciterator-polyfill": "^1.0.2",
"@bam.tech/react-native-image-resizer": "3.0.11",
"@callstack/repack": "~5.2.1",