Bug 934367 - Implement createFile for Directory. r=dhylands
authorYuan Xulei <xyuan@mozilla.com>
Wed, 05 Mar 2014 16:40:48 +0800
changeset 197887 7010324e6153de232e01fe53ce555ca85f84fdce
parent 197886 57da6f0d047e935a1242fde9d8c09372dd7a3ceb
child 197888 43fa416d3ec387ecf4b5acaadf377764a0d90326
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdhylands
bugs934367
milestone31.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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;
+};