* refactor: implement custom ExpoImage wrapper for cache control Add ExpoImage component with automatic cacheKey/cachePath management and replace all expo-image imports across the app * refactor(ios): convert Gekidou to CocoaPods Migrate from Swift Package Manager to CocoaPods, add Keychain write operations, refactor notification handler to remove react-native-notifications headers, and upgrade Swift to 5.0 * npm audit * update fastlane * feat(ci): integrate Intune MAM for enterprise builds with strict OSS protection Add Intune submodule, CI actions, Fastlane configuration, developer scripts, pre-commit hooks, and validation workflows to enable internal MAM builds while protecting OSS repository * fix tests by mocking @mattermost/intune * feat: implement Intune MAM integration with comprehensive security enforcement Add IntuneManager, refactor SecurityManager/SessionManager for MAM policies, implement native OIDC auth flow, add biometric enforcement, conditional launch blocking, and file protection controls * fix alerts when no server database is present * Unify cache strategy * fix emit config changed after it was stored in the db * Handle Mid-Session Enrollment Detection * fix ADALLogOverrideDisabled missing in Fastfile * fix flow for initial enrollment * fix and add unit tests * enable Intune configuration for PR and beta builds, CLIENT_ID should be changed before actual release * Update intune submodule with addressed feedback * fix validate-intune-clean workflow * feat(intune): add comprehensive error handling and SAML+Entra support Add production-ready error handling for native Entra authentication with user-friendly i18n messages, comprehensive test coverage, and support for Entra login when server requires SAML. * update i18n * update intune submodule * update build-pr token * fix race condition between server auth and intune enrollment * fix CI workflow to build with intune * use deploy key for intune submodule * set the config directly in the submodule .git * debug injection * try setting GIT_SSH_COMMAND * remove action debug * fix server url input * match pod cache with intune hash * Fastfile and envs * have workflows check for intune/.git * have ci cache intune frameworks as well * update Fastlane to set no-cache to artifacts uploaded * fix s3 upload * fix pblist template * Attempt to remove the cache control for PR uploads to s3 * use hash from commit for S3 path * Implement crash-resilient selective wipe with automatic retry and add removeInternetPassword to Gekidou Keychain * Fix surface errors from intune login * fix postinstall scripts * use cacheKey for draft md images * remove unnecessary double await during test * Have isMinimumLicenseTier accept valid license sku tier as target * Add missing Auth error messages * remove the last period for intune errors in i18n * do not call unenroll with wipe on manual logout * Fix tests and Intune error messages * do not filter any SSO type regardless of which is used for Intune * fix 412 to not retry * fix tests, app logs sharing and share_extension avatar cache * apply setScreenCapturePolicy on license change Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * re-apply screen capture on enrollment Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * use userData from intunr login and prevent getMe Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com> * Check for Biometrics and Jailbreak as we used to --------- Co-authored-by: Eva Sarafianou <eva.sarafianou@mattermost.com>
627 lines
19 KiB
TypeScript
627 lines
19 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import Model from '@nozbe/watermelondb/Model';
|
|
import {applicationName} from 'expo-application';
|
|
import {
|
|
cacheDirectory, deleteAsync, documentDirectory, getInfoAsync,
|
|
type FileInfo as ExpoFileInfo,
|
|
} from 'expo-file-system';
|
|
import mimeDB from 'mime-db';
|
|
import {Alert, Linking, Platform} from 'react-native';
|
|
import Permissions, {PERMISSIONS} from 'react-native-permissions';
|
|
|
|
import {Files} from '@constants';
|
|
import {generateId} from '@utils/general';
|
|
import keyMirror from '@utils/key_mirror';
|
|
import {logError} from '@utils/log';
|
|
import {deleteEntitiesFile, getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
|
import {urlSafeBase64Encode} from '@utils/security';
|
|
|
|
import type {PastedFile} from '@mattermost/react-native-paste-input';
|
|
import type FileModel from '@typings/database/models/servers/file';
|
|
import type {IntlShape} from 'react-intl';
|
|
import type {DocumentPickerResponse} from 'react-native-document-picker';
|
|
import type {Asset} from 'react-native-image-picker';
|
|
|
|
const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
|
|
const CONTENT_DISPOSITION_REGEXP = /inline;filename=".*\.([a-z]+)";/i;
|
|
const DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024;// 50 Mb
|
|
|
|
export const FileFilters = keyMirror({
|
|
ALL: null,
|
|
DOCUMENTS: null,
|
|
SPREADSHEETS: null,
|
|
PRESENTATIONS: null,
|
|
CODE: null,
|
|
IMAGES: null,
|
|
AUDIO: null,
|
|
VIDEOS: null,
|
|
});
|
|
export type FileFilter = keyof typeof FileFilters
|
|
|
|
export const GENERAL_SUPPORTED_DOCS_FORMAT = [
|
|
'application/json',
|
|
'application/msword',
|
|
'application/pdf',
|
|
'application/rtf',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.ms-powerpoint',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'application/x-x509-ca-cert',
|
|
'application/xml',
|
|
'text/csv',
|
|
'text/plain',
|
|
];
|
|
|
|
const SUPPORTED_DOCS_FORMAT = Platform.select({
|
|
android: GENERAL_SUPPORTED_DOCS_FORMAT,
|
|
ios: [
|
|
...GENERAL_SUPPORTED_DOCS_FORMAT,
|
|
'application/vnd.apple.pages',
|
|
'application/vnd.apple.numbers',
|
|
'application/vnd.apple.keynote',
|
|
],
|
|
});
|
|
|
|
const SUPPORTED_IMAGE_FORMAT = ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'svg', 'xcf', 'gif'];
|
|
|
|
const SUPPORTED_VIDEO_FORMAT = Platform.select({
|
|
ios: ['video/mp4', 'video/x-m4v', 'video/quicktime'],
|
|
android: ['video/3gpp', 'video/x-matroska', 'video/mp4', 'video/webm', 'video/quicktime'],
|
|
});
|
|
|
|
const SUPPORTED_AUDIO_FORMAT = Platform.select({
|
|
ios: ['audio/mp4', 'audio/mpeg', 'audio/wav', 'audio/x-aiff'],
|
|
android: ['audio/mp4', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/webm', 'audio/3gpp'],
|
|
});
|
|
|
|
const types: Record<string, string> = {};
|
|
const extensions: Record<string, readonly string[]> = {};
|
|
|
|
export function filterFileExtensions(filter?: FileFilter): string {
|
|
let searchTerms: string[] = [];
|
|
switch (filter) {
|
|
case FileFilters.ALL:
|
|
return '';
|
|
case FileFilters.DOCUMENTS:
|
|
searchTerms = Files.DOCUMENT_TYPES;
|
|
break;
|
|
case FileFilters.SPREADSHEETS:
|
|
searchTerms = Files.SPREADSHEET_TYPES;
|
|
break;
|
|
case FileFilters.PRESENTATIONS:
|
|
searchTerms = Files.PRESENTATION_TYPES;
|
|
break;
|
|
case FileFilters.CODE:
|
|
searchTerms = Files.CODE_TYPES;
|
|
break;
|
|
case FileFilters.IMAGES:
|
|
searchTerms = Files.IMAGE_TYPES;
|
|
break;
|
|
case FileFilters.AUDIO:
|
|
searchTerms = Files.AUDIO_TYPES;
|
|
break;
|
|
case FileFilters.VIDEOS:
|
|
searchTerms = Files.VIDEO_TYPES;
|
|
break;
|
|
default:
|
|
return '';
|
|
}
|
|
return 'ext:' + searchTerms.join(' ext:');
|
|
}
|
|
|
|
/**
|
|
* Populate the extensions and types maps.
|
|
* @private
|
|
*/
|
|
|
|
function populateMaps() {
|
|
// source preference (least -> most)
|
|
const preference = ['nginx', 'apache', undefined, 'iana'];
|
|
|
|
Object.keys(mimeDB).forEach((type) => {
|
|
const mime = mimeDB[type];
|
|
const exts = mime.extensions;
|
|
|
|
if (!exts || !exts.length) {
|
|
return;
|
|
}
|
|
|
|
extensions[type] = exts;
|
|
|
|
for (let i = 0; i < exts.length; i++) {
|
|
const extension = exts[i];
|
|
|
|
if (types[extension]) {
|
|
const from = preference.indexOf(mimeDB[types[extension]].source);
|
|
const to = preference.indexOf(mime.source);
|
|
|
|
if (types[extension] !== 'application/octet-stream' &&
|
|
(from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
types[extension] = type;
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function deleteV1Data() {
|
|
const dir = Platform.OS === 'ios' ? getIOSAppGroupDetails().appGroupSharedDirectory : documentDirectory;
|
|
|
|
try {
|
|
const directory = `${dir}/mmkv`;
|
|
const mmkvDirInfo = await getInfoAsync(directory);
|
|
if (mmkvDirInfo.exists) {
|
|
await deleteAsync(directory);
|
|
}
|
|
} catch {
|
|
// do nothing
|
|
}
|
|
|
|
try {
|
|
const entitiesInfo = await getInfoAsync(`${dir}/entities`);
|
|
if (entitiesInfo.exists) {
|
|
deleteEntitiesFile();
|
|
}
|
|
} catch (e) {
|
|
// do nothing
|
|
}
|
|
}
|
|
|
|
export async function deleteFileCache(serverUrl: string) {
|
|
const serverDir = urlSafeBase64Encode(serverUrl);
|
|
return deleteFileCacheByDir(serverDir);
|
|
}
|
|
|
|
export async function deleteFileCacheByDir(dir: string) {
|
|
if (Platform.OS === 'ios') {
|
|
const appGroupCacheDir = `${getIOSAppGroupDetails().appGroupSharedDirectory}/Library/Caches/${dir}`;
|
|
await deleteFilesInDir(appGroupCacheDir);
|
|
}
|
|
|
|
const cacheDir = `${cacheDirectory}/${dir}`;
|
|
await deleteFilesInDir(cacheDir);
|
|
|
|
return true;
|
|
}
|
|
|
|
async function deleteFilesInDir(directory: string) {
|
|
if (directory) {
|
|
const cacheDirInfo = await getInfoAsync(directory);
|
|
if (cacheDirInfo.exists) {
|
|
await deleteAsync(directory, {idempotent: true});
|
|
}
|
|
}
|
|
}
|
|
|
|
export function lookupMimeType(filename: string) {
|
|
if (!Object.keys(extensions).length) {
|
|
populateMaps();
|
|
}
|
|
|
|
const ext = filename.split('.').pop()?.toLowerCase();
|
|
return types[ext!] || 'application/octet-stream';
|
|
}
|
|
|
|
export function getExtensionFromMime(type: string) {
|
|
if (!Object.keys(extensions).length) {
|
|
populateMaps();
|
|
}
|
|
|
|
if (!type || typeof type !== 'string') {
|
|
return undefined;
|
|
}
|
|
|
|
const match = EXTRACT_TYPE_REGEXP.exec(type);
|
|
|
|
// get extensions
|
|
const exts = match && extensions[match[1].toLowerCase()];
|
|
|
|
if (!exts || !exts.length) {
|
|
return undefined;
|
|
}
|
|
|
|
return exts[0];
|
|
}
|
|
|
|
export function getExtensionFromContentDisposition(contentDisposition: string) {
|
|
const match = CONTENT_DISPOSITION_REGEXP.exec(contentDisposition);
|
|
let extension = match && match[1];
|
|
if (extension) {
|
|
if (!Object.keys(types).length) {
|
|
populateMaps();
|
|
}
|
|
|
|
extension = extension.toLowerCase();
|
|
if (types[extension]) {
|
|
return extension;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export const getAllowedServerMaxFileSize = (config: ClientConfig) => {
|
|
return config && config.MaxFileSize ? parseInt(config.MaxFileSize, 10) : DEFAULT_SERVER_MAX_FILE_SIZE;
|
|
};
|
|
|
|
export const isGif = (file?: FileInfo | FileModel) => {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
let mime = 'mime_type' in file ? file.mime_type : file.mimeType;
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return mime === 'image/gif';
|
|
};
|
|
|
|
export function extractExtension(filename: string) {
|
|
const ext = filename.split('.').pop() || '';
|
|
return ext.startsWith('.') ? ext.slice(1).toLowerCase() : ext.toLowerCase();
|
|
}
|
|
|
|
export const isImage = (file?: FileInfo | FileModel) => {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
if (isGif(file)) {
|
|
return true;
|
|
}
|
|
|
|
const fileExt = extractExtension(file.extension || file.name);
|
|
|
|
if (!SUPPORTED_IMAGE_FORMAT.includes(fileExt)) {
|
|
return false;
|
|
}
|
|
|
|
let mimeType = 'mime_type' in file ? file.mime_type : file.mimeType;
|
|
if (!mimeType) {
|
|
mimeType = lookupMimeType(file.extension) || lookupMimeType(file.name);
|
|
}
|
|
|
|
return Boolean(mimeType?.startsWith('image/'));
|
|
};
|
|
|
|
export const isDocument = (file?: FileInfo | FileModel) => {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
let mime = 'mime_type' in file ? file.mime_type : file.mimeType;
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return SUPPORTED_DOCS_FORMAT!.includes(mime);
|
|
};
|
|
|
|
export const isPdf = (file?: FileInfo | FileModel) => {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
let mime = 'mime_type' in file ? file.mime_type : file.mimeType;
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return mime === 'application/pdf';
|
|
};
|
|
|
|
export const isVideo = (file?: FileInfo | FileModel) => {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
let mime = 'mime_type' in file ? file.mime_type : file.mimeType;
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return SUPPORTED_VIDEO_FORMAT!.includes(mime);
|
|
};
|
|
|
|
export const isAudio = (file?: FileInfo | FileModel) => {
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
let mime = 'mime_type' in file ? file.mime_type : file.mimeType;
|
|
if (mime && mime.includes(';')) {
|
|
mime = mime.split(';')[0];
|
|
} else if (!mime && file?.name) {
|
|
mime = lookupMimeType(file.name);
|
|
}
|
|
|
|
return SUPPORTED_AUDIO_FORMAT!.includes(mime);
|
|
};
|
|
|
|
export function getFormattedFileSize(bytes: number): string {
|
|
const fileSizes: Array<[string, number]> = [
|
|
['TB', 1024 * 1024 * 1024 * 1024],
|
|
['GB', 1024 * 1024 * 1024],
|
|
['MB', 1024 * 1024],
|
|
['KB', 1024],
|
|
];
|
|
const size = fileSizes.find((unitAndMinBytes) => {
|
|
const minBytes = unitAndMinBytes[1];
|
|
return bytes > minBytes;
|
|
});
|
|
|
|
if (size) {
|
|
return `${Math.floor(bytes / (size[1]))} ${size[0]}`;
|
|
}
|
|
|
|
return `${bytes} B`;
|
|
}
|
|
|
|
export function getFileType(file: FileInfo | ExtractedFileInfo): string {
|
|
if (!file || !file.extension) {
|
|
return 'other';
|
|
}
|
|
|
|
const fileExt = file.extension.toLowerCase();
|
|
const fileTypes = [
|
|
'image',
|
|
'code',
|
|
'pdf',
|
|
'video',
|
|
'audio',
|
|
'spreadsheet',
|
|
'text',
|
|
'word',
|
|
'presentation',
|
|
'patch',
|
|
'zip',
|
|
];
|
|
|
|
return fileTypes.find((fileType) => {
|
|
const constForFileTypeExtList = `${fileType}_types`.toUpperCase();
|
|
const fileTypeExts = Files[constForFileTypeExtList];
|
|
return fileTypeExts.indexOf(fileExt) > -1;
|
|
}) || 'other';
|
|
}
|
|
|
|
export function getLocalFilePathFromFile(serverUrl: string, file: FileInfo | FileModel) {
|
|
const fileIdPath = file.id?.replace(/[^0-9a-z]/g, '');
|
|
if (serverUrl) {
|
|
const server = urlSafeBase64Encode(serverUrl);
|
|
const hasValidFilename = file?.name && !file.name.includes('/');
|
|
const hasValidExtension = file?.extension && !file.extension.includes('/');
|
|
if (hasValidFilename) {
|
|
let extension: string | undefined = file.extension;
|
|
let filename = file.name;
|
|
|
|
if (!hasValidExtension) {
|
|
const mimeType = (file instanceof Model) ? file.mimeType : file.mime_type;
|
|
extension = getExtensionFromMime(mimeType);
|
|
}
|
|
|
|
if (extension && filename.includes(`.${extension}`)) {
|
|
filename = filename.replace(`.${extension}`, '');
|
|
} else {
|
|
const fileParts = file.name.split('.');
|
|
|
|
if (fileParts.length > 1) {
|
|
extension = fileParts.pop();
|
|
filename = fileParts.join('.');
|
|
}
|
|
}
|
|
|
|
return `${cacheDirectory}${server}/Files/${filename}-${fileIdPath}.${extension}`;
|
|
} else if (file?.id && hasValidExtension) {
|
|
return `${cacheDirectory}${server}/Files/${fileIdPath}.${file.extension}`;
|
|
} else if (file?.id) {
|
|
return `${cacheDirectory}${server}/Files/${fileIdPath}`;
|
|
}
|
|
}
|
|
|
|
throw new Error('File path could not be set');
|
|
}
|
|
|
|
export async function extractFileInfo(files: Array<Asset | DocumentPickerResponse | PastedFile>) {
|
|
const out: ExtractedFileInfo[] = [];
|
|
|
|
await Promise.all(files.map(async (file) => {
|
|
if (!file || !file.uri) {
|
|
logError('extractFileInfo no file or url');
|
|
return;
|
|
}
|
|
|
|
const outFile = {
|
|
progress: 0,
|
|
localPath: file.uri,
|
|
clientId: generateId(),
|
|
loading: true,
|
|
} as unknown as ExtractedFileInfo;
|
|
|
|
if ('fileSize' in file) {
|
|
outFile.size = file.fileSize || 0;
|
|
outFile.name = file.fileName || '';
|
|
} else {
|
|
const localPath = file.uri || '';
|
|
try {
|
|
const fileInfo = await getInfoAsync(localPath, {size: true});
|
|
if ('size' in fileInfo) {
|
|
outFile.size = fileInfo.size || 0;
|
|
outFile.name = localPath.substring(localPath.lastIndexOf('/') + 1);
|
|
}
|
|
} catch (e) {
|
|
logError('extractFileInfo', e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (file.type) {
|
|
outFile.mime_type = file.type;
|
|
} else {
|
|
outFile.mime_type = lookupMimeType(outFile.name);
|
|
}
|
|
|
|
out.push(outFile);
|
|
}));
|
|
|
|
return out;
|
|
}
|
|
|
|
export function fileSizeWarning(intl: IntlShape, maxFileSize: number) {
|
|
return intl.formatMessage({
|
|
id: 'file_upload.fileAbove',
|
|
defaultMessage: 'Files must be less than {max}',
|
|
}, {
|
|
max: getFormattedFileSize(maxFileSize),
|
|
});
|
|
}
|
|
|
|
export function fileMaxWarning(intl: IntlShape, maxFileCount: number) {
|
|
return intl.formatMessage({
|
|
id: 'mobile.file_upload.max_warning',
|
|
defaultMessage: 'Uploads limited to {count} files maximum.',
|
|
}, {
|
|
count: maxFileCount,
|
|
});
|
|
}
|
|
|
|
export function uploadDisabledWarning(intl: IntlShape) {
|
|
return intl.formatMessage({
|
|
id: 'mobile.file_upload.disabled2',
|
|
defaultMessage: 'File uploads from mobile are disabled.',
|
|
});
|
|
}
|
|
|
|
export const fileExists = async (path: string) => {
|
|
try {
|
|
const file = await getInfoAsync(path);
|
|
return file.exists;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const hasWriteStoragePermission = async (intl: IntlShape) => {
|
|
if (Platform.OS === 'android' && Platform.Version < 33) {
|
|
const storagePermission = PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE;
|
|
let permissionRequest;
|
|
const hasPermissionToStorage = await Permissions.check(storagePermission);
|
|
switch (hasPermissionToStorage) {
|
|
case Permissions.RESULTS.DENIED:
|
|
permissionRequest = await Permissions.request(storagePermission);
|
|
return permissionRequest === Permissions.RESULTS.GRANTED;
|
|
case Permissions.RESULTS.BLOCKED: {
|
|
const title = intl.formatMessage(
|
|
{
|
|
id: 'mobile.storage_permission_denied_title',
|
|
defaultMessage:
|
|
'{applicationName} would like to access your files',
|
|
},
|
|
{applicationName},
|
|
);
|
|
const text = intl.formatMessage(
|
|
{
|
|
id: 'mobile.write_storage_permission_denied_description',
|
|
defaultMessage:
|
|
'Save files to your device. Open Settings to grant {applicationName} write access to files on this device.',
|
|
},
|
|
{applicationName},
|
|
);
|
|
|
|
Alert.alert(title, text, [
|
|
{
|
|
text: intl.formatMessage({
|
|
id: 'mobile.permission_denied_dismiss',
|
|
defaultMessage: "Don't Allow",
|
|
}),
|
|
},
|
|
{
|
|
text: intl.formatMessage({
|
|
id: 'mobile.permission_denied_retry',
|
|
defaultMessage: 'Settings',
|
|
}),
|
|
onPress: () => Linking.openSettings(),
|
|
},
|
|
]);
|
|
return false;
|
|
}
|
|
default: return true;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
export const getAllFilesInCachesDirectory = async (serverUrl: string) => {
|
|
try {
|
|
const files: ExpoFileInfo[] = [];
|
|
|
|
const promises = [getInfoAsync(`${cacheDirectory}/${urlSafeBase64Encode(serverUrl)}`, {size: true})];
|
|
if (Platform.OS === 'ios') {
|
|
const cacheDir = `${getIOSAppGroupDetails().appGroupSharedDirectory}/Library/Caches/${urlSafeBase64Encode(serverUrl)}`;
|
|
promises.push(getInfoAsync(cacheDir, {size: true}));
|
|
}
|
|
|
|
const dirs = await Promise.allSettled(promises);
|
|
dirs.forEach((p) => {
|
|
if (p.status === 'fulfilled' && 'size' in p.value) {
|
|
files.push(p.value);
|
|
}
|
|
});
|
|
|
|
const flattenedFiles = files.flat();
|
|
const totalSize = flattenedFiles.reduce((acc, file) => {
|
|
if ('size' in file) {
|
|
return acc + file.size;
|
|
}
|
|
return acc;
|
|
}, 0);
|
|
return {
|
|
files: flattenedFiles,
|
|
totalSize,
|
|
};
|
|
} catch (error) {
|
|
return {error};
|
|
}
|
|
};
|
|
|
|
export const pathWithPrefix = (prefix: string, path: string) => {
|
|
const p = path.startsWith(prefix) ? '' : prefix;
|
|
return `${p}${path}`;
|
|
};
|
|
|
|
export const deleteFile = async (path: string) => {
|
|
await deleteAsync(pathWithPrefix('file://', path));
|
|
};
|
|
|
|
export const filesLocalPathValidation = async (files: FileModel[], authorId: string) => {
|
|
const filesInfo: FileInfo[] = [];
|
|
for await (const f of files) {
|
|
const info = f.toFileInfo(authorId);
|
|
if (info.localPath) {
|
|
const exists = await fileExists(info.localPath);
|
|
if (!exists) {
|
|
info.localPath = '';
|
|
}
|
|
}
|
|
filesInfo.push(info);
|
|
}
|
|
|
|
return filesInfo;
|
|
};
|