1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5'use strict'; 6 7// Metadata is stored in files as serialized to JSON maps. See contents of 8// example1.fake and example2.fake. 9 10// Multiple volumes can be opened at the same time. The key is the 11// fileSystemId, which is the same as the file's displayPath. 12// The value is a Volume object. 13var volumes = {}; 14 15// Defines a volume object that contains information about a mounted file 16// system. 17function Volume(entry, metadata, opt_openedFiles) { 18 // Used for restoring the opened file entry after resuming the event page. 19 this.entry = entry; 20 21 // The volume metadata. 22 this.metadata = []; 23 for (var path in metadata) { 24 this.metadata[path] = metadata[path]; 25 // Date object is serialized in JSON as string. 26 this.metadata[path].modificationTime = 27 new Date(metadata[path].modificationTime); 28 } 29 30 // A map with currently opened files. The key is a requestId value from the 31 // openFileRequested event, and the value is the file path. 32 this.openedFiles = opt_openedFiles ? opt_openedFiles : {}; 33}; 34 35function onUnmountRequested(options, onSuccess, onError) { 36 if (Object.keys(volumes[options.fileSystemId].openedFiles).length != 0) { 37 onError('IN_USE'); 38 return; 39 } 40 41 chrome.fileSystemProvider.unmount( 42 {fileSystemId: options.fileSystemId}, 43 function() { 44 delete volumes[options.fileSystemId]; 45 saveState(); // Remove volume from local storage state. 46 onSuccess(); 47 }, 48 function() { 49 onError('FAILED'); 50 }); 51}; 52 53function onGetMetadataRequested(options, onSuccess, onError) { 54 restoreState(options.fileSystemId, function () { 55 var entryMetadata = 56 volumes[options.fileSystemId].metadata[options.entryPath]; 57 if (!entryMetadata) 58 error('NOT_FOUND'); 59 else 60 onSuccess(entryMetadata); 61 }, onError); 62}; 63 64function onReadDirectoryRequested(options, onSuccess, onError) { 65 restoreState(options.fileSystemId, function () { 66 var directoryMetadata = 67 volumes[options.fileSystemId].metadata[options.directoryPath]; 68 if (!directoryMetadata) { 69 onError('NOT_FOUND'); 70 return; 71 } 72 if (!directoryMetadata.isDirectory) { 73 onError('NOT_A_DIRECTORY'); 74 return; 75 } 76 77 // Retrieve directory contents from metadata. 78 var entries = []; 79 for (var entry in volumes[options.fileSystemId].metadata) { 80 // Do not add itself on the list. 81 if (entry == options.directoryPath) 82 continue; 83 // Check if the entry is a child of the requested directory. 84 if (entry.indexOf(options.directoryPath) != 0) 85 continue; 86 // Restrict to direct children only. 87 if (entry.substring(options.directoryPath.length + 1).indexOf('/') != -1) 88 continue; 89 90 entries.push(volumes[options.fileSystemId].metadata[entry]); 91 } 92 onSuccess(entries, false /* Last call. */); 93 }, onError); 94}; 95 96function onOpenFileRequested(options, onSuccess, onError) { 97 restoreState(options.fileSystemId, function () { 98 if (options.mode != 'READ' || options.create) { 99 onError('INVALID_OPERATION'); 100 } else { 101 volumes[options.fileSystemId].openedFiles[options.requestId] = 102 options.filePath; 103 onSuccess(); 104 } 105 }, onError); 106}; 107 108function onCloseFileRequested(options, onSuccess, onError) { 109 restoreState(options.fileSystemId, function () { 110 if (!volumes[options.fileSystemId].openedFiles[options.openRequestId]) { 111 onError('INVALID_OPERATION'); 112 } else { 113 delete volumes[options.fileSystemId].openedFiles[options.openRequestId]; 114 onSuccess(); 115 } 116 }, onError); 117}; 118 119function onReadFileRequested(options, onSuccess, onError) { 120 restoreState(options.fileSystemId, function () { 121 var filePath = 122 volumes[options.fileSystemId].openedFiles[options.openRequestId]; 123 if (!filePath) { 124 onError('INVALID_OPERATION'); 125 return; 126 } 127 128 var contents = volumes[options.fileSystemId].metadata[filePath].contents; 129 130 // Write the contents as ASCII text. 131 var buffer = new ArrayBuffer(options.length); 132 var bufferView = new Uint8Array(buffer); 133 for (var i = 0; i < options.length; i++) { 134 bufferView[i] = contents.charCodeAt(i); 135 } 136 137 onSuccess(buffer, false /* Last call. */); 138 }, onError); 139}; 140 141// Saves state in case of restarts, event page suspend, crashes, etc. 142function saveState() { 143 var state = {}; 144 for (var volumeId in volumes) { 145 var entryId = chrome.fileSystem.retainEntry(volumes[volumeId].entry); 146 state[volumeId] = { 147 entryId: entryId, 148 openedFiles: volumes[volumeId].openedFiles 149 }; 150 } 151 chrome.storage.local.set({state: state}); 152} 153 154// Restores metadata for the passed file system ID. 155function restoreState(fileSystemId, onSuccess, onError) { 156 chrome.storage.local.get(['state'], function(result) { 157 // Check if metadata for the given file system is alread in memory. 158 if (volumes[fileSystemId]) { 159 onSuccess(); 160 return; 161 } 162 163 chrome.fileSystem.restoreEntry( 164 result.state[fileSystemId].entryId, 165 function(entry) { 166 readMetadataFromFile(entry, 167 function(metadata) { 168 volumes[fileSystemId] = new Volume(entry, metadata, 169 result.state[fileSystemId].openedFiles); 170 onSuccess(); 171 }, onError); 172 }); 173 }); 174} 175 176// Reads metadata from a file and returns it with the onSuccess callback. 177function readMetadataFromFile(entry, onSuccess, onError) { 178 entry.file(function(file) { 179 var fileReader = new FileReader(); 180 fileReader.onload = function(event) { 181 onSuccess(JSON.parse(event.target.result)); 182 }; 183 184 fileReader.onerror = function(event) { 185 onError('FAILED'); 186 }; 187 188 fileReader.readAsText(file); 189 }); 190} 191 192// Event called on opening a file with the extension or mime type 193// declared in the manifest file. 194chrome.app.runtime.onLaunched.addListener(function(event) { 195 event.items.forEach(function(item) { 196 readMetadataFromFile(item.entry, 197 function(metadata) { 198 // Mount the volume and save its information in local storage 199 // in order to be able to recover the metadata in case of 200 // restarts, system crashes, etc. 201 chrome.fileSystem.getDisplayPath(item.entry, function(displayPath) { 202 volumes[displayPath] = new Volume(item.entry, metadata); 203 chrome.fileSystemProvider.mount( 204 {fileSystemId: displayPath, displayName: item.entry.name}, 205 function() { saveState(); }, 206 function() { console.error('Failed to mount.'); }); 207 }); 208 }, 209 function(error) { 210 console.error(error); 211 }); 212 }); 213}); 214 215// Event called on a profile startup. 216chrome.runtime.onStartup.addListener(function () { 217 chrome.storage.local.get(['state'], function(result) { 218 // Nothing to change. 219 if (!result.state) 220 return; 221 222 // Remove files opened before the profile shutdown from the local storage. 223 for (var volumeId in result.state) { 224 result.state[volumeId].openedFiles = {}; 225 } 226 chrome.storage.local.set({state: result.state}); 227 }); 228}); 229 230// Save the state before suspending the event page, so we can resume it 231// once new events arrive. 232chrome.runtime.onSuspend.addListener(function() { 233 saveState(); 234}); 235 236chrome.fileSystemProvider.onUnmountRequested.addListener( 237 onUnmountRequested); 238chrome.fileSystemProvider.onGetMetadataRequested.addListener( 239 onGetMetadataRequested); 240chrome.fileSystemProvider.onReadDirectoryRequested.addListener( 241 onReadDirectoryRequested); 242chrome.fileSystemProvider.onOpenFileRequested.addListener( 243 onOpenFileRequested); 244chrome.fileSystemProvider.onCloseFileRequested.addListener( 245 onCloseFileRequested); 246chrome.fileSystemProvider.onReadFileRequested.addListener( 247 onReadFileRequested); 248