Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3516409
D7252.id24592.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
5 KB
Referenced Files
None
Subscribers
None
D7252.id24592.diff
View Options
diff --git a/lib/media/file-utils.js b/lib/media/file-utils.js
--- a/lib/media/file-utils.js
+++ b/lib/media/file-utils.js
@@ -195,6 +195,14 @@
return match.toLowerCase();
}
+function filenameWithoutExtension(filename: string): string {
+ const extension = extensionFromFilename(filename);
+ if (!extension) {
+ return filename;
+ }
+ return filename.replace(new RegExp(`\\.${extension}$`), '');
+}
+
const pathRegex = /^file:\/\/(.*)$/;
function pathFromURI(uri: string): ?string {
const matches = uri.match(pathRegex);
@@ -226,4 +234,5 @@
extensionFromFilename,
pathFromURI,
filenameFromPathOrURI,
+ filenameWithoutExtension,
};
diff --git a/lib/media/file-utils.test.js b/lib/media/file-utils.test.js
new file mode 100644
--- /dev/null
+++ b/lib/media/file-utils.test.js
@@ -0,0 +1,17 @@
+// @flow
+
+import { filenameWithoutExtension } from './file-utils.js';
+
+describe('filenameWithoutExtension', () => {
+ it('removes extension from filename', () => {
+ expect(filenameWithoutExtension('foo.jpg')).toBe('foo');
+ });
+
+ it('removes only last extension part from filename', () => {
+ expect(filenameWithoutExtension('foo.bar.jpg')).toBe('foo.bar');
+ });
+
+ it('returns filename if it has no extension', () => {
+ expect(filenameWithoutExtension('foo')).toBe('foo');
+ });
+});
diff --git a/native/media/media-cache.js b/native/media/media-cache.js
new file mode 100644
--- /dev/null
+++ b/native/media/media-cache.js
@@ -0,0 +1,120 @@
+// @flow
+
+import invariant from 'invariant';
+import fs from 'react-native-fs';
+
+import type { MediaCachePersistence } from 'lib/components/media-cache-provider.react.js';
+import {
+ extensionFromFilename,
+ filenameFromPathOrURI,
+ filenameWithoutExtension,
+ pathFromURI,
+ readableFilename,
+ replaceExtension,
+} from 'lib/media/file-utils.js';
+
+import { temporaryDirectoryPath } from './file-utils.js';
+
+const cacheDirectory = `${temporaryDirectoryPath}media-cache`;
+
+function basenameFromHolder(holder: string) {
+ // if holder is a file URI or path, use the last segment of the path
+ const holderBase = holder.split('/').pop();
+ return filenameWithoutExtension(holderBase);
+}
+
+async function ensureCacheDirectory() {
+ await fs.mkdir(cacheDirectory, {
+ // iOS only, apple rejects apps having offline cache without this attribute
+ NSURLIsExcludedFromBackupKey: true,
+ });
+}
+
+async function listCachedFiles() {
+ await ensureCacheDirectory();
+ return await fs.readdir(cacheDirectory);
+}
+
+async function getCacheSize() {
+ const files = await listCachedFiles();
+ return files.reduce((total, file) => total + file.size, 0);
+}
+
+async function hasURI(uri: string): Promise<boolean> {
+ return await fs.exists(pathFromURI(uri));
+}
+
+async function getCachedFile(holder: string) {
+ const cachedFiles = await listCachedFiles();
+ const baseHolder = basenameFromHolder(holder);
+ const cachedFile = cachedFiles.find(file =>
+ filenameWithoutExtension(file).startsWith(baseHolder),
+ );
+ if (cachedFile) {
+ return `file://${cacheDirectory}/${cachedFile}`;
+ }
+ return null;
+}
+
+async function clearCache() {
+ await fs.unlink(cacheDirectory);
+}
+
+const dataURLRegex = /^data:([^;]+);base64,([a-zA-Z0-9+/]+={0,2})$/;
+async function saveFile(holder: string, uri: string): Promise<string> {
+ await ensureCacheDirectory();
+ let filePath;
+ const baseHolder = basenameFromHolder(holder);
+ const isDataURI = uri.startsWith('data:');
+ if (isDataURI) {
+ const [, mime, data] = uri.match(dataURLRegex) ?? [];
+ invariant(mime, 'malformed data-URI: missing MIME type');
+ invariant(data, 'malformed data-URI: invalid data');
+ const filename = readableFilename(baseHolder, mime) ?? baseHolder;
+ filePath = `${cacheDirectory}/${filename}`;
+
+ await fs.writeFile(filePath, data, 'base64');
+ } else {
+ const uriFilename = filenameFromPathOrURI(uri);
+ invariant(uriFilename, 'malformed URI: missing filename');
+ const extension = extensionFromFilename(uriFilename);
+ const filename = extension
+ ? replaceExtension(baseHolder, extension)
+ : baseHolder;
+ filePath = `${cacheDirectory}/${filename}`;
+ await fs.copyFile(uri, filePath);
+ }
+ return `file://${filePath}`;
+}
+
+async function cleanupOldFiles(cacheSizeLimit: number): Promise<boolean> {
+ await ensureCacheDirectory();
+ const files = await fs.readDir(cacheDirectory);
+ let cacheSize = files.reduce((total, file) => total + file.size, 0);
+ const filesSorted = [...files].sort((a, b) => a.mtime - b.mtime);
+
+ const filenamesToRemove = [];
+ while (cacheSize > cacheSizeLimit) {
+ const file = filesSorted.shift();
+ if (!file) {
+ break;
+ }
+ filenamesToRemove.push(file.name);
+ cacheSize -= file.size;
+ }
+ await Promise.all(
+ filenamesToRemove.map(file => fs.unlink(`${cacheDirectory}/${file}`)),
+ );
+ return filenamesToRemove.length > 0;
+}
+
+const filesystemMediaCache: MediaCachePersistence = {
+ hasURI,
+ getCachedFile,
+ saveFile,
+ getCacheSize,
+ cleanupOldFiles,
+ clearCache,
+};
+
+export { filesystemMediaCache };
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Dec 23, 2:22 PM (19 h, 9 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2694868
Default Alt Text
D7252.id24592.diff (5 KB)
Attached To
Mode
D7252: [native] Implement filesystem media cache
Attached
Detach File
Event Timeline
Log In to Comment