blob: f13029090a6811af2aa72faf131f7fdb533eb815 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Namespace for the Camera app.
*/
var cca = cca || {};
/**
* Namespace for models.
*/
cca.models = cca.models || {};
/**
* Creates the file system controller.
* @constructor
*/
cca.models.FileSystem = function() {
// End of properties, seal the object.
Object.seal(this);
};
/**
* The prefix of thumbnail files.
* @type {string}
* @const
*/
cca.models.FileSystem.THUMBNAIL_PREFIX = 'thumb-';
/**
* Width of thumbnail.
* @type {number}
* @const
*/
cca.models.FileSystem.THUMBNAIL_WIDTH = 480;
/**
* Directory in the internal file system.
* @type {DirectoryEntry}
*/
cca.models.FileSystem.internalDir = null;
/**
* Temporary directory in the internal file system.
* @type {DirectoryEntry}
*/
cca.models.FileSystem.internalTempDir = null;
/**
* Directory in the external file system.
* @type {DirectoryEntry}
*/
cca.models.FileSystem.externalDir = null;
/**
* Initializes the directory in the internal file system.
* @return {!Promise<DirectoryEntry>} Promise for the directory result.
* @private
*/
cca.models.FileSystem.initInternalDir_ = function() {
return new Promise((resolve, reject) => {
webkitRequestFileSystem(
window.PERSISTENT, 768 * 1024 * 1024 /* 768MB */,
(fs) => resolve(fs.root), reject);
});
};
/**
* Initializes the temporary directory in the internal file system.
* @return {!Promise<DirectoryEntry>} Promise for the directory result.
* @private
*/
cca.models.FileSystem.initInternalTempDir_ = function() {
return new Promise((resolve, reject) => {
webkitRequestFileSystem(
window.TEMPORARY, 768 * 1024 * 1024 /* 768MB */,
(fs) => resolve(fs.root), reject);
});
};
/**
* Initializes the directory in the external file system.
* @return {!Promise<?DirectoryEntry>} Promise for the directory result.
* @private
*/
cca.models.FileSystem.initExternalDir_ = function() {
return new Promise((resolve) => {
if (!cca.util.isChromeOS()) {
resolve([null, null]);
return;
}
cca.proxy.browserProxy.getVolumeList((volumes) => {
if (volumes) {
for (var i = 0; i < volumes.length; i++) {
var volumeId = volumes[i].volumeId;
if (volumeId.indexOf('downloads:Downloads') !== -1 ||
volumeId.indexOf('downloads:MyFiles') !== -1) {
cca.proxy.browserProxy.requestFileSystem(
volumes[i], (fs) => resolve([fs && fs.root, volumeId]));
return;
}
}
}
resolve([null, null]);
});
}).then(([dir, volumeId]) => {
if (volumeId && volumeId.indexOf('downloads:MyFiles') !== -1) {
return cca.models.FileSystem.readDir_(dir).then((entries) => {
return entries.find(
(entry) => entry.name == 'Downloads' && entry.isDirectory);
});
}
return dir;
});
};
/**
* Initializes file systems, migrating pictures if needed. This function
* should be called only once in the beginning of the app.
* @param {function()} promptMigrate Callback to instantiate a promise that
prompts users to migrate pictures if no acknowledgement yet.
* @return {!Promise<boolean>} Promise for the external-fs result.
*/
cca.models.FileSystem.initialize = function(promptMigrate) {
var checkAcked = new Promise((resolve) => {
// ack 0: User has not yet acknowledged to migrate pictures.
// ack 1: User acknowledges to migrate pictures to Downloads.
cca.proxy.browserProxy.localStorageGet(
{ackMigratePictures: 0},
(values) => resolve(values.ackMigratePictures >= 1));
});
var checkMigrated = new Promise((resolve) => {
if (chrome.chromeosInfoPrivate) {
chrome.chromeosInfoPrivate.get(['cameraMediaConsolidated'],
(values) => resolve(values['cameraMediaConsolidated']));
} else {
resolve(false);
}
});
var ackMigrate = () =>
cca.proxy.browserProxy.localStorageSet({ackMigratePictures: 1});
var doneMigrate = () => chrome.chromeosInfoPrivate &&
chrome.chromeosInfoPrivate.set('cameraMediaConsolidated', true);
return Promise
.all([
cca.models.FileSystem.initInternalDir_(),
cca.models.FileSystem.initInternalTempDir_(),
cca.models.FileSystem.initExternalDir_(),
checkAcked,
checkMigrated,
])
.then(([internalDir, internalTempDir, externalDir, acked, migrated]) => {
cca.models.FileSystem.internalDir = internalDir;
cca.models.FileSystem.internalTempDir = internalTempDir;
cca.models.FileSystem.externalDir = externalDir;
if (migrated && !externalDir) {
throw new Error('External file system should be available.');
}
// Check if acknowledge-prompt and migrate-pictures are needed.
if (migrated || !cca.models.FileSystem.externalDir) {
return [false, false];
}
// Check if any internal picture other than thumbnail needs migration.
// Pictures taken by old Camera App may not have IMG_ or VID_ prefix.
var dir = cca.models.FileSystem.internalDir;
return cca.models.FileSystem.readDir_(dir)
.then((entries) => {
return entries.some(
(entry) => !cca.models.FileSystem.hasThumbnailPrefix_(entry));
})
.then((migrateNeeded) => {
if (migrateNeeded) {
return [!acked, true];
}
// If the external file system is supported and there is already
// no picture in the internal file system, it implies done
// migration and then doesn't need acknowledge-prompt.
ackMigrate();
doneMigrate();
return [false, false];
});
})
.then(
([promptNeeded, migrateNeeded]) => { // Prompt to migrate if needed.
return !promptNeeded ? migrateNeeded : promptMigrate().then(() => {
ackMigrate();
return migrateNeeded;
});
})
.then((migrateNeeded) => { // Migrate pictures if needed.
const external = cca.models.FileSystem.externalDir != null;
return !migrateNeeded ? external :
cca.models.FileSystem.migratePictures()
.then(doneMigrate)
.then(() => external);
});
};
/**
* Reads file entries from the directory.
* @param {DirectoryEntry} dir Directory entry to be read.
* @return {!Promise<!Array<FileEntry>>} Promise for the read file entries.
* @private
*/
cca.models.FileSystem.readDir_ = function(dir) {
return !dir ? Promise.resolve([]) : new Promise((resolve, reject) => {
var dirReader = dir.createReader();
var entries = [];
var readEntries = () => {
dirReader.readEntries((inEntries) => {
if (inEntries.length == 0) {
resolve(entries);
return;
}
entries = entries.concat(inEntries);
readEntries();
}, reject);
};
readEntries();
});
};
/**
* Migrates all picture-files from internal storage to external storage.
* @return {!Promise} Promise for the operation.
*/
cca.models.FileSystem.migratePictures = function() {
var internalDir = cca.models.FileSystem.internalDir;
var externalDir = cca.models.FileSystem.externalDir;
var migratePicture = (pictureEntry, thumbnailEntry) => {
var name = cca.models.FileSystem.regulatePictureName(pictureEntry);
return cca.models.FileSystem.getFile(
externalDir, name, true).then((entry) => {
return new Promise((resolve, reject) => {
pictureEntry.copyTo(externalDir, entry.name, (result) => {
if (result.name != pictureEntry.name && thumbnailEntry) {
// Thumbnails can be recreated later if failing to rename them here.
thumbnailEntry.moveTo(internalDir,
cca.models.FileSystem.getThumbnailName(result));
}
pictureEntry.remove(() => {});
resolve();
}, reject);
});
});
};
return cca.models.FileSystem.readDir_(internalDir).then((internalEntries) => {
var pictureEntries = [];
var thumbnailEntriesByName = {};
cca.models.FileSystem.parseInternalEntries_(
internalEntries, thumbnailEntriesByName, pictureEntries);
var migrated = [];
for (var index = 0; index < pictureEntries.length; index++) {
var entry = pictureEntries[index];
var thumbnailName = cca.models.FileSystem.getThumbnailName(entry);
var thumbnailEntry = thumbnailEntriesByName[thumbnailName];
migrated.push(migratePicture(entry, thumbnailEntry));
}
return Promise.all(migrated);
});
};
/**
* Regulates the picture name to the desired format if it's in legacy formats.
* @param {FileEntry} entry Picture entry whose name to be regulated.
* @return {string} Name in the desired format.
*/
cca.models.FileSystem.regulatePictureName = function(entry) {
if (cca.models.FileSystem.hasVideoPrefix(entry) ||
cca.models.FileSystem.hasImagePrefix_(entry)) {
var match = entry.name.match(/(\w{3}_\d{8}_\d{6})(?:_(\d+))?(\..+)?$/);
if (match) {
var idx = match[2] ? ' (' + match[2] + ')' : '';
var ext = match[3] ? match[3].replace(/\.webm$/, '.mkv') : '';
return match[1] + idx + ext;
}
} else {
// Early pictures are in legacy file name format (crrev.com/c/310064).
var match = entry.name.match(/(\d+).(?:\d+)/);
if (match) {
return (new cca.models.Filenamer(parseInt(match[1], 10))).newImageName();
}
}
return entry.name;
};
/**
* Saves the blob to the given file name. Name of the actually saved file
* might be different from the given file name if the file already exists.
* @param {DirectoryEntry} dir Directory to be written into.
* @param {string} name Name of the file.
* @param {!Blob} blob Data of the file to be saved.
* @return {!Promise<FileEntry>} Promise for the result.
* @private
*/
cca.models.FileSystem.saveToFile_ = function(dir, name, blob) {
return cca.models.FileSystem.getFile(dir, name, true).then((entry) => {
return new Promise((resolve, reject) => {
entry.createWriter((fileWriter) => {
fileWriter.onwriteend = () => resolve(entry);
fileWriter.onerror = reject;
fileWriter.write(blob);
}, reject);
});
});
};
/**
* Saves photo blob or metadata blob into predefined default location.
* @param {!Blob} blob Data of the photo to be saved.
* @param {string} filename Filename of the photo to be saved.
* @return {!Promise<FileEntry>} Promise for the result.
*/
cca.models.FileSystem.saveBlob = function(blob, filename) {
const dir =
cca.models.FileSystem.externalDir || cca.models.FileSystem.internalDir;
return cca.models.FileSystem.saveToFile_(dir, filename, blob);
};
/**
* Creates a file for saving temporary video recording result.
* @return {!Promise<!FileEntry>} Newly created temporary file.
* @throws {Error} If failed to create video temp file.
*/
cca.models.FileSystem.createTempVideoFile = async function() {
const dir =
cca.models.FileSystem.externalDir || cca.models.FileSystem.internalDir;
const filename = new cca.models.Filenamer().newVideoName();
const file = await cca.models.FileSystem.getFile(dir, filename, true);
if (file === null) {
throw new Error('Failed to create video temp file.');
}
return file;
};
/**
* @const {string}
*/
cca.models.FileSystem.PRIVATE_TEMPFILE_NAME = 'video-intent.mkv';
/**
* @return {!Promise<!FileEntry>} Newly created temporary file.
* @throws {Error} If failed to create video temp file.
*/
cca.models.FileSystem.createPrivateTempVideoFile = async function() {
// TODO(inker): Handles running out of space case.
const dir = cca.models.FileSystem.internalTempDir;
const file = await cca.models.FileSystem.getFile(
dir, cca.models.FileSystem.PRIVATE_TEMPFILE_NAME, true);
if (file === null) {
throw new Error('Failed to create private video temp file.');
}
return file;
};
/**
* Saves temporary video file to predefined default location.
* @param {FileEntry} tempfile Temporary video file to be saved.
* @param {string} filename Filename to be saved.
* @return {Promise<?FileEntry>} Saved video file.
*/
cca.models.FileSystem.saveVideo = async function(tempfile, filename) {
var dir =
cca.models.FileSystem.externalDir || cca.models.FileSystem.internalDir;
if (!dir) {
return await null;
}
// Non-null version for the Closure Compiler.
let nonNullDir = dir;
// Assuming content of tempfile contains all recorded chunks appended together
// and is a well-formed video. The work needed here is just to move the file
// to the correct directory and rename as the specified filename.
if (tempfile.name == filename) {
return tempfile;
}
return new Promise(
(resolve, reject) =>
tempfile.moveTo(nonNullDir, filename, resolve, reject));
};
/**
* Gets the thumbnail name of the given picture.
* @param {FileEntry} entry Picture's file entry.
* @return {string} Thumbnail name.
*/
cca.models.FileSystem.getThumbnailName = function(entry) {
var thumbnailName = cca.models.FileSystem.THUMBNAIL_PREFIX + entry.name;
return (thumbnailName.substr(0, thumbnailName.lastIndexOf('.')) ||
thumbnailName) + '.jpg';
};
/**
* Creates and saves the thumbnail of the given picture.
* @param {boolean} isVideo Picture is a video.
* @param {FileEntry} entry Picture's file entry whose thumbnail to be saved.
* @return {!Promise<FileEntry>} Promise for the result.
*/
cca.models.FileSystem.saveThumbnail = function(isVideo, entry) {
return cca.models.FileSystem.pictureURL(entry)
.then((url) => {
return cca.util.scalePicture(
url, isVideo, cca.models.FileSystem.THUMBNAIL_WIDTH);
})
.then((blob) => {
var thumbnailName = cca.models.FileSystem.getThumbnailName(entry);
return cca.models.FileSystem.saveToFile_(
cca.models.FileSystem.internalDir, thumbnailName, blob);
});
};
/**
* Checks if the entry's name has the video prefix.
* @param {FileEntry} entry File entry.
* @return {boolean} Has the video prefix or not.
*/
cca.models.FileSystem.hasVideoPrefix = function(entry) {
return entry.name.startsWith(cca.models.Filenamer.VIDEO_PREFIX);
};
/**
* Checks if the entry's name has the image prefix.
* @param {FileEntry} entry File entry.
* @return {boolean} Has the image prefix or not.
* @private
*/
cca.models.FileSystem.hasImagePrefix_ = function(entry) {
return entry.name.startsWith(cca.models.Filenamer.IMAGE_PREFIX);
};
/**
* Checks if the entry's name has the thumbnail prefix.
* @param {FileEntry} entry File entry.
* @return {boolean} Has the thumbnail prefix or not.
* @private
*/
cca.models.FileSystem.hasThumbnailPrefix_ = function(entry) {
return entry.name.startsWith(cca.models.FileSystem.THUMBNAIL_PREFIX);
};
/**
* Parses and filters the internal entries to thumbnail and picture entries.
* @param {Array<FileEntry>} internalEntries Internal file entries.
* @param {Object<string, FileEntry>} thumbnailEntriesByName Result thumbanil
* entries mapped by thumbnail names, initially empty.
* @param {Array<FileEntry>=} pictureEntries Result picture entries, initially
* empty.
* @private
*/
cca.models.FileSystem.parseInternalEntries_ = function(
internalEntries, thumbnailEntriesByName, pictureEntries) {
var isThumbnail = cca.models.FileSystem.hasThumbnailPrefix_;
var thumbnailEntries = [];
if (pictureEntries) {
for (var index = 0; index < internalEntries.length; index++) {
if (isThumbnail(internalEntries[index])) {
thumbnailEntries.push(internalEntries[index]);
} else {
pictureEntries.push(internalEntries[index]);
}
}
} else {
thumbnailEntries = internalEntries.filter(isThumbnail);
}
for (var index = 0; index < thumbnailEntries.length; index++) {
var thumbnailEntry = thumbnailEntries[index];
thumbnailEntriesByName[thumbnailEntry.name] = thumbnailEntry;
}
};
/**
* Gets the picture and thumbnail entries.
* @return {!Promise<!Array<!Array<FileEntry>|!Object<string, FileEntry>>>}
* Promise for the picture entries and the thumbnail entries mapped by
* thumbnail names.
*/
cca.models.FileSystem.getEntries = function() {
return Promise.all([
cca.models.FileSystem.readDir_(cca.models.FileSystem.internalDir),
cca.models.FileSystem.readDir_(cca.models.FileSystem.externalDir),
]).then(([internalEntries, externalEntries]) => {
var pictureEntries = [];
var thumbnailEntriesByName = {};
if (cca.models.FileSystem.externalDir) {
pictureEntries = externalEntries.filter((entry) => {
if (!cca.models.FileSystem.hasVideoPrefix(entry) &&
!cca.models.FileSystem.hasImagePrefix_(entry)) {
return false;
}
return entry.name.match(/_(\d{8})_(\d{6})(?: \((\d+)\))?/);
});
cca.models.FileSystem.parseInternalEntries_(
internalEntries, thumbnailEntriesByName);
} else {
cca.models.FileSystem.parseInternalEntries_(
internalEntries, thumbnailEntriesByName, pictureEntries);
}
return [pictureEntries, thumbnailEntriesByName];
});
};
/**
* Returns an URL for a picture.
* @param {FileEntry} entry File entry.
* @return {!Promise<string>} Promise for the result.
*/
cca.models.FileSystem.pictureURL = function(entry) {
return new Promise((resolve) => {
if (cca.models.FileSystem.externalDir) {
entry.file((file) => resolve(URL.createObjectURL(file)));
} else {
resolve(entry.toURL());
}
});
};
/**
* Gets the file by the given name, avoiding name conflicts if necessary.
* @param {DirectoryEntry} dir Directory to get the file from.
* @param {string} name File name. Result file may have a different name.
* @param {boolean} create True to create file, false otherwise.
* @return {!Promise<?FileEntry>} Promise for the result.
*/
cca.models.FileSystem.getFile = function(dir, name, create) {
return new Promise((resolve, reject) => {
var options = create ? {create: true, exclusive: true} : {create: false};
dir.getFile(name, options, resolve, reject);
}).catch((error) => {
if (create && error.name == 'InvalidModificationError') {
// Avoid name conflicts for creating files.
return cca.models.FileSystem.getFile(dir,
cca.models.FileSystem.incrementFileName_(name), create);
} else if (!create && error.name == 'NotFoundError') {
return null;
}
throw error;
});
};
/**
* Increments the file index of a given file name to avoid name conflicts.
* @param {string} name File name.
* @return {string} File name with incremented index.
* @private
*/
cca.models.FileSystem.incrementFileName_ = function(name) {
var [base, ext] = ['', ''];
var idx = 0;
var match = name.match(/^([^.]+)(\..+)?$/);
if (match) {
base = match[1];
ext = match[2];
match = base.match(/ \((\d+)\)$/);
if (match) {
base = base.substring(0, match.index);
idx = parseInt(match[1], 10);
}
}
return base + ' (' + (idx + 1) + ')' + ext;
};