Page MenuHomePhabricator

D7252.id24592.diff
No OneTemporary

D7252.id24592.diff

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

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)

Event Timeline