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