Bug 934367 - Implement createFile for Directory. r=dhylands
authorYuan Xulei <xyuan@mozilla.com>
Wed, 05 Mar 2014 16:40:48 +0800
changeset 179797 7010324e6153de232e01fe53ce555ca85f84fdce
parent 179796 57da6f0d047e935a1242fde9d8c09372dd7a3ceb
child 179798 43fa416d3ec387ecf4b5acaadf377764a0d90326
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersdhylands
bugs934367
milestone31.0a1
Bug 934367 - Implement createFile for Directory. r=dhylands
dom/devicestorage/test/mochitest.ini
dom/devicestorage/test/test_fs_app_permissions.html
dom/devicestorage/test/test_fs_createFile.html
dom/filesystem/CreateFileTask.cpp
dom/filesystem/CreateFileTask.h
dom/filesystem/Directory.cpp
dom/filesystem/Directory.h
dom/filesystem/FileSystemRequestParent.cpp
dom/filesystem/moz.build
dom/ipc/PContent.ipdl
dom/permission/tests/test_keyboard.html
dom/webidl/Directory.webidl
--- a/dom/devicestorage/test/mochitest.ini
+++ b/dom/devicestorage/test/mochitest.ini
@@ -24,8 +24,9 @@ support-files = devicestorage_common.js
 [test_watch.html]
 [test_watchOther.html]
 
 # FileSystem API tests
 [test_fs_basic.html]
 [test_fs_createDirectory.html]
 [test_fs_get.html]
 [test_fs_remove.html]
+[test_fs_createFile.html]
--- a/dom/devicestorage/test/test_fs_app_permissions.html
+++ b/dom/devicestorage/test/test_fs_app_permissions.html
@@ -26,16 +26,36 @@ function randomFilename(l) {
   let result = "";
   for (let i=0; i<l; i++) {
     let r = Math.floor(set.length * Math.random());
     result += set.substring(r, r + 1);
   }
   return result;
 }
 
+function getRandomBuffer() {
+  var size = 1024;
+  var buffer = new ArrayBuffer(size);
+  var view = new Uint8Array(buffer);
+  for (var i = 0; i < size; i++) {
+    view[i] = parseInt(Math.random() * 255);
+  }
+  return buffer;
+}
+
+function createRandomBlob(mime) {
+  let size = 1024;
+  let buffer = new ArrayBuffer(size);
+  let view = new Uint8Array(buffer);
+  for (let i = 0; i < size; i++) {
+    view[i] = parseInt(Math.random() * 255);
+  }
+  return blob = new Blob([buffer], {type: mime});
+}
+
 let MockPermissionPrompt = SpecialPowers.MockPermissionPrompt;
 MockPermissionPrompt.init();
 
 SimpleTest.waitForExplicitFinish();
 
 function TestCreateDirectory(iframe, data) {
   function cbError(e) {
     is(e.name, "SecurityError", "[TestCreateDirectory] Should fire a SecurityError for type " + data.type);
@@ -83,16 +103,44 @@ function TestGet(iframe, data) {
     ok(true, "[TestGet] Success callback of getRoot was called for type " + data.type);
     root.get("testfile" + data.fileExtension).then(function() {
       is(data.shouldPass, true, "[TestGet] Success callback was called for type " + data.type);
       testComplete(iframe, data);
     }, cbError);
   }, cbError);
 }
 
+function TestCreateFile(iframe, data) {
+  function cbError(e) {
+    is(e.name, "SecurityError", "[TestCreateFile] Should fire a SecurityError for type " + data.type);
+    is(data.shouldPass, false, "[TestCreateFile] Error callback was called for type " + data.type + '. Error: ' + e.name);
+    testComplete(iframe, data);
+  }
+
+  let storage = iframe.contentDocument.defaultView.navigator.getDeviceStorage(data.type);
+  isnot(storage, null, "[TestCreateFile] Should be able to get storage object for " + data.type);
+
+  if (!storage) {
+    testComplete(iframe, data);
+    return;
+  }
+
+  storage.getRoot().then(function(root) {
+    ok(true, "[TestCreateFile] Success callback of getRoot was called for type " + data.type);
+    let filename = randomFilename(100) + data.fileExtension;
+    root.createFile(filename, {
+      data: createRandomBlob(data.mimeType),
+      ifExists: "replace"
+    }).then(function() {
+      is(data.shouldPass, true, "[TestCreateFile] Success callback was called for type " + data.type);
+      testComplete(iframe, data);
+    }, cbError);
+  }, cbError);
+}
+
 function TestRemove(iframe, data) {
   function cbError(e) {
     is(e.name, "SecurityError", "[TestRemove] Should fire a SecurityError for type " + data.type);
     is(data.shouldPass, false, "[TestRemove] Error callback was called for type " + data.type + '. Error: ' + e.name);
     testComplete(iframe, data);
   }
 
   createTestFile(data.fileExtension);
@@ -367,16 +415,192 @@ let gData = [
     shouldPass: true,
 
     app: "https://example.com/manifest_cert.webapp",
     permissions: ["device-storage:sdcard"],
 
     test: TestCreateDirectory
   },
 
+  // Directory#createFile
+
+  // Web applications with no permissions
+  {
+    type: 'pictures',
+    mimeType: 'image/png',
+    shouldPass: false,
+    fileExtension: '.png',
+    test: TestCreateFile
+  },
+  {
+    type: 'videos',
+    mimeType: 'video/ogv',
+    shouldPass: false,
+    fileExtension: '.ogv',
+    test: TestCreateFile
+  },
+  {
+    type: 'videos',
+    mimeType: 'video/ogg',
+    shouldPass: false,
+    fileExtension: '.ogg',
+    test: TestCreateFile
+  },
+  {
+    type: 'music',
+    mimeType: 'audio/ogg',
+    shouldPass: false,
+    fileExtension: '.ogg',
+    test: TestCreateFile
+  },
+  {
+    type: 'music',
+    mimeType: 'audio/ogg',
+    shouldPass: false,
+    fileExtension: '.txt',
+    test: TestCreateFile
+  },
+  {
+    type: 'sdcard',
+    mimeType: 'text/plain',
+    shouldPass: false,
+    fileExtension: '.txt',
+    test: TestCreateFile
+  },
+
+  // Web applications with permission granted
+  {
+    type: 'pictures',
+    mimeType: 'image/png',
+    shouldPass: true,
+    fileExtension: '.png',
+
+    permissions: ["device-storage:pictures"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'videos',
+    mimeType: 'video/ogv',
+    shouldPass: true,
+    fileExtension: '.ogv',
+
+    permissions: ["device-storage:videos"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'videos',
+    mimeType: 'video/ogg',
+    shouldPass: true,
+    fileExtension: '.ogg',
+
+    permissions: ["device-storage:videos"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'music',
+    mimeType: 'audio/ogg',
+    shouldPass: true,
+    fileExtension: '.ogg',
+
+    permissions: ["device-storage:music"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'music',
+    mimeType: 'audio/ogg',
+    shouldPass: false,
+    fileExtension: '.txt',
+
+    permissions: ["device-storage:music"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'sdcard',
+    mimeType: 'text/plain',
+    shouldPass: true,
+    fileExtension: '.txt',
+
+    permissions: ["device-storage:sdcard"],
+
+    test: TestCreateFile
+  },
+
+  // Certified application with permision granted
+  {
+    type: 'pictures',
+    mimeType: 'image/png',
+    shouldPass: true,
+    fileExtension: '.png',
+
+    app: "https://example.com/manifest_cert.webapp",
+    permissions: ["device-storage:pictures"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'videos',
+    mimeType: 'video/ogv',
+    shouldPass: true,
+    fileExtension: '.ogv',
+
+    app: "https://example.com/manifest_cert.webapp",
+    permissions: ["device-storage:videos"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'videos',
+    mimeType: 'video/ogg',
+    shouldPass: true,
+    fileExtension: '.ogg',
+
+    app: "https://example.com/manifest_cert.webapp",
+    permissions: ["device-storage:videos"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'music',
+    mimeType: 'audio/ogg',
+    shouldPass: true,
+    fileExtension: '.ogg',
+
+    app: "https://example.com/manifest_cert.webapp",
+    permissions: ["device-storage:music"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'music',
+    mimeType: 'audio/ogg',
+    shouldPass: false,
+    fileExtension: '.txt',
+
+    app: "https://example.com/manifest_cert.webapp",
+    permissions: ["device-storage:music"],
+
+    test: TestCreateFile
+  },
+  {
+    type: 'sdcard',
+    mimeType: 'text/plain',
+    shouldPass: true,
+    fileExtension: '.txt',
+
+    app: "https://example.com/manifest_cert.webapp",
+    permissions: ["device-storage:sdcard"],
+
+    test: TestCreateFile
+  },
+
   // Directory#remove
 
   // Web applications with no permissions
   {
     type: 'pictures',
     shouldPass: false,
     fileExtension: '.png',
     test: TestRemove
new file mode 100644
--- /dev/null
+++ b/dom/devicestorage/test/test_fs_createFile.html
@@ -0,0 +1,132 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html> <!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=910412
+-->
+<head>
+  <title>Test createDirectory of the FileSystem API for device storage</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="devicestorage_common.js"></script>
+
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=910412">Mozilla Bug 910412</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+devicestorage_setup();
+
+let gTestCount = 0;
+let gFileReader = new FileReader();
+let gRoot;
+
+function str2array(str) {
+  let strlen = str.length;
+  let buf = new ArrayBuffer(strlen);
+  let bufView = new Uint8Array(buf);
+  for (let i=0; i < strlen; i++) {
+    bufView[i] = str.charCodeAt(i);
+  }
+  return buf;
+}
+
+function array2str(data) {
+  return String.fromCharCode.apply(String, new Uint8Array(data));
+}
+
+let gTestCases = [
+  // Create with string data.
+  {
+    text: "My name is Yuan.",
+    get data() { return this.text; },
+    shouldPass: true,
+    mode: "replace"
+  },
+
+  // Create with array buffer data.
+  {
+    text: "I'm from Kunming.",
+    get data() { return str2array(this.text); },
+    shouldPass: true,
+    mode: "replace"
+  },
+
+  // Create with array buffer view data.
+  {
+    text: "Kunming is in Yunnan province of China.",
+    get data() { return Uint8Array(str2array(this.text)); },
+    shouldPass: true,
+    mode: "replace"
+  },
+
+  // Create with blob data.
+  {
+    text: "Kunming is in Yunnan province of China.",
+    get data() { return new Blob([this.text], {type: 'image/png'}); },
+    shouldPass: true,
+    mode: "replace"
+  },
+
+  // Don't overwrite existing file.
+  {
+    data: null,
+    shouldPass: false,
+    mode: "fail"
+  }
+];
+
+function next() {
+  if (gTestCount >= gTestCases.length) {
+    devicestorage_cleanup();
+    return;
+  }
+  let c = gTestCases[gTestCount++];
+  gRoot.createFile("text.png", {
+    data: c.data,
+    ifExists: c.mode
+  }).then(function(file) {
+    is(c.shouldPass, true, "[" + gTestCount + "] Success callback was called for createFile.");
+    if (!c.shouldPass) {
+      SimpleTest.executeSoon(next);
+      return;
+    }
+    // Check the file content.
+    gFileReader.readAsArrayBuffer(file);
+    gFileReader.onload = function(e) {
+      ab = e.target.result;
+      is(array2str(e.target.result), c.text, "[" + gTestCount + "] Wrong values.");
+      SimpleTest.executeSoon(next);
+    };
+  }, function(e) {
+    is(c.shouldPass, false, "[" + gTestCount + "] Error callback was called for createFile.");
+    SimpleTest.executeSoon(next);
+  });
+}
+
+ok(navigator.getDeviceStorage, "Should have getDeviceStorage.");
+
+let storage = navigator.getDeviceStorage("pictures");
+ok(storage, "Should have gotten a storage.");
+
+// Get the root directory
+storage.getRoot().then(function(dir) {
+  ok(dir, "Should have gotten the root directory.");
+  gRoot = dir;
+  next();
+}, function(e) {
+  ok(false, e.name + " error should not arrive here!");
+  devicestorage_cleanup();
+});
+
+</script>
+</pre>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/dom/filesystem/CreateFileTask.cpp
@@ -0,0 +1,321 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CreateFileTask.h"
+
+#include <algorithm>
+
+#include "DOMError.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/FileSystemBase.h"
+#include "mozilla/dom/FileSystemUtils.h"
+#include "mozilla/dom/Promise.h"
+#include "nsDOMFile.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsStringGlue.h"
+
+namespace mozilla {
+namespace dom {
+
+uint32_t CreateFileTask::sOutputBufferSize = 0;
+
+CreateFileTask::CreateFileTask(FileSystemBase* aFileSystem,
+                               const nsAString& aPath,
+                               nsIDOMBlob* aBlobData,
+                               InfallibleTArray<uint8_t>& aArrayData,
+                               bool replace)
+  : FileSystemTaskBase(aFileSystem)
+  , mTargetRealPath(aPath)
+  , mBlobData(aBlobData)
+  , mReplace(replace)
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  MOZ_ASSERT(aFileSystem);
+  GetOutputBufferSize();
+  if (mBlobData) {
+    nsresult rv = mBlobData->GetInternalStream(getter_AddRefs(mBlobStream));
+    NS_WARN_IF(NS_FAILED(rv));
+  }
+  mArrayData.SwapElements(aArrayData);
+  nsCOMPtr<nsIGlobalObject> globalObject =
+    do_QueryInterface(aFileSystem->GetWindow());
+  if (!globalObject) {
+    return;
+  }
+  mPromise = new Promise(globalObject);
+}
+
+CreateFileTask::CreateFileTask(FileSystemBase* aFileSystem,
+                       const FileSystemCreateFileParams& aParam,
+                       FileSystemRequestParent* aParent)
+  : FileSystemTaskBase(aFileSystem, aParam, aParent)
+  , mReplace(false)
+{
+  MOZ_ASSERT(FileSystemUtils::IsParentProcess(),
+             "Only call from parent process!");
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  MOZ_ASSERT(aFileSystem);
+  GetOutputBufferSize();
+
+  mTargetRealPath = aParam.realPath();
+
+  mReplace = aParam.replace();
+
+  auto& data = aParam.data();
+
+  if (data.type() == FileSystemFileDataValue::TArrayOfuint8_t) {
+    mArrayData = data;
+    return;
+  }
+
+  BlobParent* bp = static_cast<BlobParent*>(static_cast<PBlobParent*>(data));
+  mBlobData = bp->GetBlob();
+  MOZ_ASSERT(mBlobData, "mBlobData should not be null.");
+  nsresult rv = mBlobData->GetInternalStream(getter_AddRefs(mBlobStream));
+  NS_WARN_IF(NS_FAILED(rv));
+}
+
+CreateFileTask::~CreateFileTask()
+{
+  MOZ_ASSERT(!mPromise || NS_IsMainThread(),
+             "mPromise should be released on main thread!");
+  if (mBlobStream) {
+    mBlobStream->Close();
+  }
+}
+
+already_AddRefed<Promise>
+CreateFileTask::GetPromise()
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  return nsRefPtr<Promise>(mPromise).forget();
+}
+
+FileSystemParams
+CreateFileTask::GetRequestParams(const nsString& aFileSystem) const
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  FileSystemCreateFileParams param;
+  param.filesystem() = aFileSystem;
+  param.realPath() = mTargetRealPath;
+  param.replace() = mReplace;
+  if (mBlobData) {
+    BlobChild* actor
+      = ContentChild::GetSingleton()->GetOrCreateActorForBlob(mBlobData);
+    if (actor) {
+      param.data() = actor;
+    }
+  } else {
+    param.data() = mArrayData;
+  }
+  return param;
+}
+
+FileSystemResponseValue
+CreateFileTask::GetSuccessRequestResult() const
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  BlobParent* actor = GetBlobParent(mTargetFile);
+  if (!actor) {
+    return FileSystemErrorResponse(NS_ERROR_DOM_FILESYSTEM_UNKNOWN_ERR);
+  }
+  FileSystemFileResponse response;
+  response.blobParent() = actor;
+  return response;
+}
+
+void
+CreateFileTask::SetSuccessRequestResult(const FileSystemResponseValue& aValue)
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  FileSystemFileResponse r = aValue;
+  BlobChild* actor = static_cast<BlobChild*>(r.blobChild());
+  nsCOMPtr<nsIDOMBlob> blob = actor->GetBlob();
+  mTargetFile = do_QueryInterface(blob);
+}
+
+nsresult
+CreateFileTask::Work()
+{
+  class AutoClose
+  {
+  public:
+    AutoClose(nsIOutputStream* aStream)
+      : mStream(aStream)
+    {
+      MOZ_ASSERT(aStream);
+    }
+
+    ~AutoClose()
+    {
+      mStream->Close();
+    }
+  private:
+    nsCOMPtr<nsIOutputStream> mStream;
+  };
+
+  MOZ_ASSERT(FileSystemUtils::IsParentProcess(),
+             "Only call from parent process!");
+  MOZ_ASSERT(!NS_IsMainThread(), "Only call on worker thread!");
+
+  if (mFileSystem->IsShutdown()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsCOMPtr<nsIFile> file = mFileSystem->GetLocalFile(mTargetRealPath);
+  if (!file) {
+    return NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR;
+  }
+
+  if (!mFileSystem->IsSafeFile(file)) {
+    return NS_ERROR_DOM_SECURITY_ERR;
+  }
+
+  bool exists = false;
+  nsresult rv = file->Exists(&exists);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (exists) {
+    bool isFile = false;
+    rv = file->IsFile(&isFile);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    if (!isFile) {
+      return NS_ERROR_DOM_FILESYSTEM_TYPE_MISMATCH_ERR;
+    }
+
+    if (!mReplace) {
+      return NS_ERROR_DOM_FILESYSTEM_PATH_EXISTS_ERR;
+    }
+
+    // Remove the old file before creating.
+    rv = file->Remove(false);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+  rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<nsIOutputStream> outputStream;
+  rv = NS_NewLocalFileOutputStream(getter_AddRefs(outputStream), file);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  AutoClose acOutputStream(outputStream);
+
+  nsCOMPtr<nsIOutputStream> bufferedOutputStream;
+  rv = NS_NewBufferedOutputStream(getter_AddRefs(bufferedOutputStream),
+                                  outputStream,
+                                  sOutputBufferSize);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  AutoClose acBufferedOutputStream(bufferedOutputStream);
+
+  if (mBlobStream) {
+    // Write the file content from blob data.
+
+    uint64_t bufSize = 0;
+    rv = mBlobStream->Available(&bufSize);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    while (bufSize && !mFileSystem->IsShutdown()) {
+      uint32_t written = 0;
+      uint32_t writeSize = bufSize < UINT32_MAX ? bufSize : UINT32_MAX;
+      rv = bufferedOutputStream->WriteFrom(mBlobStream, writeSize, &written);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+      bufSize -= written;
+    }
+
+    mBlobStream->Close();
+    mBlobStream = nullptr;
+
+    if (mFileSystem->IsShutdown()) {
+      return NS_ERROR_FAILURE;
+    }
+
+    mTargetFile = new nsDOMFileFile(file);
+    return NS_OK;
+  }
+
+  // Write file content from array data.
+
+  uint32_t written;
+  rv = bufferedOutputStream->Write(
+    reinterpret_cast<char*>(mArrayData.Elements()),
+    mArrayData.Length(),
+    &written);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  if (mArrayData.Length() != written) {
+    return NS_ERROR_DOM_FILESYSTEM_UNKNOWN_ERR;
+  }
+
+  mTargetFile = new nsDOMFileFile(file);
+  return NS_OK;
+}
+
+void
+CreateFileTask::HandlerCallback()
+{
+  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread!");
+  if (mFileSystem->IsShutdown()) {
+    mPromise = nullptr;
+    return;
+  }
+
+  if (HasError()) {
+    nsRefPtr<DOMError> domError = new DOMError(mFileSystem->GetWindow(),
+      mErrorValue);
+    mPromise->MaybeReject(domError);
+    mPromise = nullptr;
+    return;
+  }
+
+  mPromise->MaybeResolve(mTargetFile);
+  mPromise = nullptr;
+}
+
+void
+CreateFileTask::GetPermissionAccessType(nsCString& aAccess) const
+{
+  if (mReplace) {
+    aAccess.AssignLiteral("write");
+    return;
+  }
+
+  aAccess.AssignLiteral("create");
+}
+
+void
+CreateFileTask::GetOutputBufferSize() const
+{
+  if (sOutputBufferSize || !FileSystemUtils::IsParentProcess()) {
+    return;
+  }
+  sOutputBufferSize =
+    mozilla::Preferences::GetUint("dom.filesystem.outputBufferSize", 4096 * 4);
+}
+
+} // namespace dom
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/filesystem/CreateFileTask.h
@@ -0,0 +1,76 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CreateFileTask_h
+#define mozilla_dom_CreateFileTask_h
+
+#include "mozilla/dom/FileSystemTaskBase.h"
+#include "nsAutoPtr.h"
+
+class nsIDOMBlob;
+class nsIInputStream;
+
+namespace mozilla {
+namespace dom {
+
+class Promise;
+
+class CreateFileTask MOZ_FINAL
+  : public FileSystemTaskBase
+{
+public:
+  CreateFileTask(FileSystemBase* aFileSystem,
+                 const nsAString& aPath,
+                 nsIDOMBlob* aBlobData,
+                 InfallibleTArray<uint8_t>& aArrayData,
+                 bool replace);
+  CreateFileTask(FileSystemBase* aFileSystem,
+                 const FileSystemCreateFileParams& aParam,
+                 FileSystemRequestParent* aParent);
+
+  virtual
+  ~CreateFileTask();
+
+  already_AddRefed<Promise>
+  GetPromise();
+
+  virtual void
+  GetPermissionAccessType(nsCString& aAccess) const MOZ_OVERRIDE;
+
+protected:
+  virtual FileSystemParams
+  GetRequestParams(const nsString& aFileSystem) const MOZ_OVERRIDE;
+
+  virtual FileSystemResponseValue
+  GetSuccessRequestResult() const MOZ_OVERRIDE;
+
+  virtual void
+  SetSuccessRequestResult(const FileSystemResponseValue& aValue) MOZ_OVERRIDE;
+
+  virtual nsresult
+  Work() MOZ_OVERRIDE;
+
+  virtual void
+  HandlerCallback() MOZ_OVERRIDE;
+
+private:
+  void
+  GetOutputBufferSize() const;
+
+  static uint32_t sOutputBufferSize;
+  nsRefPtr<Promise> mPromise;
+  nsString mTargetRealPath;
+  nsCOMPtr<nsIDOMBlob> mBlobData;
+  nsCOMPtr<nsIInputStream> mBlobStream;
+  InfallibleTArray<uint8_t> mArrayData;
+  bool mReplace;
+  nsCOMPtr<nsIDOMFile> mTargetFile;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_CreateFileTask_h
--- a/dom/filesystem/Directory.cpp
+++ b/dom/filesystem/Directory.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/dom/Directory.h"
 
 #include "CreateDirectoryTask.h"
+#include "CreateFileTask.h"
 #include "FileSystemPermissionRequest.h"
 #include "GetFileOrDirectoryTask.h"
 #include "RemoveTask.h"
 
 #include "nsCharSeparatedTokenizer.h"
 #include "nsString.h"
 #include "mozilla/dom/DirectoryBinding.h"
 #include "mozilla/dom/FileSystemBase.h"
@@ -19,16 +20,21 @@
 #include "mozilla/dom/UnionTypes.h"
 
 // Resolve the name collision of Microsoft's API name with macros defined in
 // Windows header files. Undefine the macro of CreateDirectory to avoid
 // Directory#CreateDirectory being replaced by Directory#CreateDirectoryW.
 #ifdef CreateDirectory
 #undef CreateDirectory
 #endif
+// Undefine the macro of CreateFile to avoid Directory#CreateFile being replaced
+// by Directory#CreateFileW.
+#ifdef CreateFile
+#undef CreateFile
+#endif
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(Directory)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(Directory)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(Directory)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Directory)
@@ -84,16 +90,54 @@ Directory::GetName(nsString& aRetval) co
     return;
   }
 
   aRetval = Substring(mPath,
                       mPath.RFindChar(FileSystemUtils::kSeparatorChar) + 1);
 }
 
 already_AddRefed<Promise>
+Directory::CreateFile(const nsAString& aPath, const CreateFileOptions& aOptions)
+{
+  nsresult error = NS_OK;
+  nsString realPath;
+  nsRefPtr<nsIDOMBlob> blobData;
+  InfallibleTArray<uint8_t> arrayData;
+  bool replace = (aOptions.mIfExists == CreateIfExistsMode::Replace);
+
+  // Get the file content.
+  if (aOptions.mData.WasPassed()) {
+    auto& data = aOptions.mData.Value();
+    if (data.IsString()) {
+      NS_ConvertUTF16toUTF8 str(data.GetAsString());
+      arrayData.AppendElements(reinterpret_cast<const uint8_t *>(str.get()),
+                               str.Length());
+    } else if (data.IsArrayBuffer()) {
+      ArrayBuffer& buffer = data.GetAsArrayBuffer();
+      arrayData.AppendElements(buffer.Data(), buffer.Length());
+    } else if (data.IsArrayBufferView()){
+      ArrayBufferView& view = data.GetAsArrayBufferView();
+      arrayData.AppendElements(view.Data(), view.Length());
+    } else {
+      blobData = data.GetAsBlob();
+    }
+  }
+
+  if (!DOMPathToRealPath(aPath, realPath)) {
+    error = NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR;
+  }
+
+  nsRefPtr<CreateFileTask> task = new CreateFileTask(mFileSystem, realPath,
+    blobData, arrayData, replace);
+  task->SetError(error);
+  FileSystemPermissionRequest::RequestForTask(task);
+  return task->GetPromise();
+}
+
+already_AddRefed<Promise>
 Directory::CreateDirectory(const nsAString& aPath)
 {
   nsresult error = NS_OK;
   nsString realPath;
   if (!DOMPathToRealPath(aPath, realPath)) {
     error = NS_ERROR_DOM_FILESYSTEM_INVALID_PATH_ERR;
   }
   nsRefPtr<CreateDirectoryTask> task = new CreateDirectoryTask(
--- a/dom/filesystem/Directory.h
+++ b/dom/filesystem/Directory.h
@@ -16,20 +16,26 @@
 #include "nsWrapperCache.h"
 
 // Resolve the name collision of Microsoft's API name with macros defined in
 // Windows header files. Undefine the macro of CreateDirectory to avoid
 // Directory#CreateDirectory being replaced by Directory#CreateDirectoryW.
 #ifdef CreateDirectory
 #undef CreateDirectory
 #endif
+// Undefine the macro of CreateFile to avoid Directory#CreateFile being replaced
+// by Directory#CreateFileW.
+#ifdef CreateFile
+#undef CreateFile
+#endif
 
 namespace mozilla {
 namespace dom {
 
+class CreateFileOptions;
 class FileSystemBase;
 class Promise;
 class StringOrFileOrDirectory;
 
 class Directory MOZ_FINAL
   : public nsISupports
   , public nsWrapperCache
 {
@@ -51,16 +57,19 @@ public:
 
   virtual JSObject*
   WrapObject(JSContext* aCx) MOZ_OVERRIDE;
 
   void
   GetName(nsString& aRetval) const;
 
   already_AddRefed<Promise>
+  CreateFile(const nsAString& aPath, const CreateFileOptions& aOptions);
+
+  already_AddRefed<Promise>
   CreateDirectory(const nsAString& aPath);
 
   already_AddRefed<Promise>
   Get(const nsAString& aPath);
 
   already_AddRefed<Promise>
   Remove(const StringOrFileOrDirectory& aPath);
 
--- a/dom/filesystem/FileSystemRequestParent.cpp
+++ b/dom/filesystem/FileSystemRequestParent.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et cindent: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 #include "mozilla/dom/FileSystemRequestParent.h"
 
 #include "CreateDirectoryTask.h"
+#include "CreateFileTask.h"
 #include "GetFileOrDirectoryTask.h"
 #include "RemoveTask.h"
 
 #include "mozilla/AppProcessChecker.h"
 #include "mozilla/dom/FileSystemBase.h"
 
 namespace mozilla {
 namespace dom {
@@ -35,16 +36,17 @@ bool
 FileSystemRequestParent::Dispatch(ContentParent* aParent,
                                   const FileSystemParams& aParams)
 {
   MOZ_ASSERT(aParent, "aParent should not be null.");
   nsRefPtr<FileSystemTaskBase> task;
   switch (aParams.type()) {
 
     FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(CreateDirectory)
+    FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(CreateFile)
     FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(GetFileOrDirectory)
     FILESYSTEM_REQUEST_PARENT_DISPATCH_ENTRY(Remove)
 
     default: {
       NS_RUNTIMEABORT("not reached");
       break;
     }
   }
--- a/dom/filesystem/moz.build
+++ b/dom/filesystem/moz.build
@@ -10,16 +10,17 @@ EXPORTS.mozilla.dom += [
     'FileSystemBase.h',
     'FileSystemRequestParent.h',
     'FileSystemTaskBase.h',
     'FileSystemUtils.h',
 ]
 
 SOURCES += [
     'CreateDirectoryTask.cpp',
+    'CreateFileTask.cpp',
     'DeviceStorageFileSystem.cpp',
     'Directory.cpp',
     'FileSystemBase.cpp',
     'FileSystemPermissionRequest.cpp',
     'FileSystemRequestParent.cpp',
     'FileSystemTaskBase.cpp',
     'FileSystemUtils.cpp',
     'GetFileOrDirectoryTask.cpp',
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -196,16 +196,30 @@ union FMRadioRequestParams
 };
 
 struct FileSystemCreateDirectoryParams
 {
   nsString filesystem;
   nsString realPath;
 };
 
+union FileSystemFileDataValue
+{
+  uint8_t[];
+  PBlob;
+};
+
+struct FileSystemCreateFileParams
+{
+  nsString filesystem;
+  nsString realPath;
+  FileSystemFileDataValue data;
+  bool replace;
+};
+
 struct FileSystemGetFileOrDirectoryParams
 {
   nsString filesystem;
   nsString realPath;
 };
 
 union FileSystemPathOrFileValue
 {
@@ -219,16 +233,17 @@ struct FileSystemRemoveParams
   nsString directory;
   FileSystemPathOrFileValue target;
   bool recursive;
 };
 
 union FileSystemParams
 {
   FileSystemCreateDirectoryParams;
+  FileSystemCreateFileParams;
   FileSystemGetFileOrDirectoryParams;
   FileSystemRemoveParams;
 };
 
 union PrefValue {
   nsCString;
   int32_t;
   bool;
--- a/dom/permission/tests/test_keyboard.html
+++ b/dom/permission/tests/test_keyboard.html
@@ -1,9 +1,9 @@
-<!DOCTYPE HTML>
+-<!DOCTYPE HTML>
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=920977
 -->
 <head>
   <meta charset="utf-8">
   <title>Test for Bug 920977 </title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
--- a/dom/webidl/Directory.webidl
+++ b/dom/webidl/Directory.webidl
@@ -17,16 +17,36 @@ interface File;
 [NoInterfaceObject]
 interface Directory {
   /*
    * The leaf name of the directory.
    */
   readonly attribute DOMString name;
 
   /*
+   * Creates a new file or replaces an existing file with given data. The file
+   * should be a descendent of current directory.
+   *
+   * @param path The relative path of the new file to current directory.
+   * @param options It has two optional properties, 'ifExists' and 'data'.
+   * If 'ifExists' is 'fail' and the path already exists, createFile must fail;
+   * If 'ifExists' is 'replace', the path already exists, and is a file, create
+   * a new file to replace the existing one;
+   * If 'ifExists' is 'replace', the path already exists, but is a directory,
+   * createFile must fail.
+   * Otherwise, if no other error occurs, createFile will create a new file.
+   * The 'data' property contains the new file's content.
+   * @return If succeeds, the promise is resolved with the new created
+   * File object. Otherwise, rejected with a DOM error.
+   */
+  [NewObject]
+  // Promise<File>
+  Promise createFile(DOMString path, optional CreateFileOptions options);
+
+  /*
    * Creates a descendent directory. This method will create any intermediate
    * directories specified by the path segments.
    *
    * @param path The relative path of the new directory to current directory.
    * If path exists, createDirectory must fail.
    * @return If succeeds, the promise is resolved with the new created
    * Directory object. Otherwise, rejected with a DOM error.
    */
@@ -72,8 +92,14 @@ interface Directory {
    * resolved with boolean false. If the target did exist and was successfully
    * deleted, the promise is resolved with boolean true.
    */
   [NewObject]
   // Promise<boolean>
   Promise removeDeep((DOMString or File or Directory) path);
 };
 
+enum CreateIfExistsMode { "replace", "fail" };
+
+dictionary CreateFileOptions {
+  CreateIfExistsMode ifExists = "fail";
+  (DOMString or Blob or ArrayBuffer or ArrayBufferView) data;
+};