<NME> index.js
<BEF> ADDFILE
<MSG> Merge pull request #530 from captainsafia/clone-notebook

Add ability to duplicate a notebook
<DFF> @@ -0,0 +1,24 @@
+import fs from 'fs';
+import path from 'path';
+import tmp from 'tmp';
+
+export function copyNotebook(filename) {
+  return new Promise((resolve, reject) => {
+    const readStream = fs.createReadStream(filename);
+    readStream.on('error', reject);
+
+    const base = path.basename(filename, '.ipynb');
+
+    tmp.tmpName({ prefix: `${base}-Copy`, postfix: '.ipynb' }, (err, newFilename) => {
+      if (err) {
+        reject(err);
+      }
+
+      const writeStream = fs.createWriteStream(newFilename);
+      writeStream.on('error', reject);
+
+      readStream.pipe(writeStream);
+      writeStream.on('close', resolve(newFilename));
+    });
+  });
+}

{'commit': 'diff --git a/package.json b/package.json\nindex 64931d26f..c8bc4f640 100644\n--- a/package.json\n+++ b/package.json\n@@ -78,6 +78,7 @@\n     "remark-react": "^2.1.0",\n     "rxjs": "^5.0.0-beta.6",\n     "spawnteract": "^2.0.0",\n+    "tmp": "0.0.28",\n     "transformime-react": "^1.0.0",\n     "uuid": "^2.0.2"\n   },\ndiff --git a/src/main/menu.js b/src/main/menu.js\nindex 75562b09b..27445dd54 100644\n--- a/src/main/menu.js\n+++ b/src/main/menu.js\n@@ -70,6 +70,10 @@ export const fileSubMenus = {\n     },\n     accelerator: \'CmdOrCtrl+Shift+S\',\n   },\n+  duplicate: {\n+    label: \'&Duplicate Notebook\',\n+    click: createSender(\'menu:duplicate-notebook\'),\n+  },\n };\n \n export const file = {\n@@ -404,6 +408,7 @@ export function loadFullMenu() {\n         fileSubMenus.open,\n         fileSubMenus.save,\n         fileSubMenus.saveAs,\n+        fileSubMenus.duplicate,\n       ],\n     };\n \ndiff --git a/src/notebook/menu/index.js b/src/notebook/menu/index.js\nindex 3fea64f92..1bdffcf99 100644\n--- a/src/notebook/menu/index.js\n+++ b/src/notebook/menu/index.js\n@@ -4,7 +4,7 @@ import {\n   showSaveAsDialog,\n } from \'../api/save\';\n \n-import { tildify } from \'../native-window\';\n+import { tildify, launchFilename } from \'../native-window\';\n \n import {\n   executeCell,\n@@ -20,6 +20,7 @@ import {\n   setForwardCheckpoint,\n } from \'../actions\';\n \n+import { copyNotebook } from \'../utils\';\n \n import { ipcRenderer as ipc, webFrame, remote } from \'electron\';\n const BrowserWindow = remote.BrowserWindow;\n@@ -194,6 +195,25 @@ export function dispatchZoomOut() {\n   webFrame.setZoomLevel(webFrame.getZoomLevel() - 1);\n }\n \n+export function dispatchDuplicate(store) {\n+  const state = store.getState();\n+  const { notificationSystem } = state.app;\n+  const filename = state.metadata.get(\'filename\');\n+  if (filename) {\n+    copyNotebook(filename).then((value) => {\n+      launchFilename(value);\n+    });\n+  } else {\n+    notificationSystem.addNotification({\n+      title: \'Can\\\'t Duplicate Unsaved Notebook\',\n+      message: \'A notebook must be saved before it can be duplicated.\',\n+      dismissble: true,\n+      position: \'tr\',\n+      level: \'warning\',\n+    });\n+  }\n+}\n+\n export function initMenuHandlers(store, dispatch) {\n   ipc.on(\'menu:undo\', dispatchUndo.bind(null, store, dispatch));\n   ipc.on(\'menu:redo\', dispatchRedo.bind(null, store, dispatch));\n@@ -202,6 +222,7 @@ export function initMenuHandlers(store, dispatch) {\n   ipc.on(\'menu:clear-all\', dispatchClearAll.bind(null, store, dispatch));\n   ipc.on(\'menu:save\', dispatchSave.bind(null, store, dispatch));\n   ipc.on(\'menu:save-as\', dispatchSaveAs.bind(null, store, dispatch));\n+  ipc.on(\'menu:duplicate-notebook\', dispatchDuplicate.bind(null, store));\n   ipc.on(\'menu:kill-kernel\', dispatchKillKernel.bind(null, store, dispatch));\n   ipc.on(\'menu:interrupt-kernel\', dispatchInterruptKernel.bind(null, store, dispatch));\n   ipc.on(\'menu:restart-kernel\', dispatchRestartKernel.bind(null, store, dispatch));\ndiff --git a/src/notebook/native-window/index.js b/src/notebook/native-window/index.js\nindex a58fd8113..f648ffc91 100644\n--- a/src/notebook/native-window/index.js\n+++ b/src/notebook/native-window/index.js\n@@ -1,7 +1,11 @@\n import { remote } from \'electron\';\n-const { getCurrentWindow } = remote;\n+const { BrowserWindow, getCurrentWindow } = remote;\n import home from \'home-dir\';\n+import fs from \'fs\';\n import path from \'path\';\n+import { fromJS } from \'commutable\';\n+\n+import { deferURL } from \'../../main/launch\';\n \n import Rx from \'rxjs/Rx\';\n \n@@ -42,3 +46,38 @@ export function initNativeHandlers(store) {\n       win.setTitle(res.title);\n     });\n }\n+\n+function launch(notebook, filename) {\n+  let win = new BrowserWindow({\n+    width: 800,\n+    height: 1000,\n+    title: !filename ? \'Untitled\' : path.relative(\'.\', filename.replace(/.ipynb$/, \'\')),\n+  });\n+\n+  const index = path.join(__dirname, \'..\', \'..\', \'..\', \'static\', \'index.html\');\n+  win.loadURL(`file://${index}`);\n+\n+  win.webContents.on(\'did-finish-load\', () => {\n+    win.webContents.send(\'main:load\', { notebook: notebook.toJS(), filename });\n+  });\n+\n+  win.webContents.on(\'will-navigate\', deferURL);\n+\n+  win.on(\'closed\', () => {\n+    win = null;\n+  });\n+\n+  return win;\n+}\n+\n+export function launchFilename(filename) {\n+  return new Promise((resolve, reject) => {\n+    fs.readFile(filename, {}, (err, data) => {\n+      if (err) {\n+        reject(err);\n+      } else {\n+        resolve(launch(fromJS(JSON.parse(data)), filename));\n+      }\n+    });\n+  });\n+}\ndiff --git a/src/notebook/utils/index.js b/src/notebook/utils/index.js\nnew file mode 100644\nindex 000000000..4854d9326\n--- /dev/null\n+++ b/src/notebook/utils/index.js\n@@ -0,0 +1,24 @@\n+import fs from \'fs\';\n+import path from \'path\';\n+import tmp from \'tmp\';\n+\n+export function copyNotebook(filename) {\n+  return new Promise((resolve, reject) => {\n+    const readStream = fs.createReadStream(filename);\n+    readStream.on(\'error\', reject);\n+\n+    const base = path.basename(filename, \'.ipynb\');\n+\n+    tmp.tmpName({ prefix: `${base}-Copy`, postfix: \'.ipynb\' }, (err, newFilename) => {\n+      if (err) {\n+        reject(err);\n+      }\n+\n+      const writeStream = fs.createWriteStream(newFilename);\n+      writeStream.on(\'error\', reject);\n+\n+      readStream.pipe(writeStream);\n+      writeStream.on(\'close\', resolve(newFilename));\n+    });\n+  });\n+}\n', 'commit_subject': 'Merge pull request #530 from captainsafia/clone-notebook', 'repo_name': 'nteract/nteract', 'committer': {'name': 'GitHub', 'email': '1505422b2465e9a84f6fdfaa161078890c593f06@github.com', 'time_sec': '1467328349', 'tz_offset': '-420', 'date': {'seconds': '1467328349'}}, 'commit_tree': '5de96626f7403c11db8fa7185cb89318ba880373', 'commit_message': 'Merge pull request #530 from captainsafia/clone-notebook\n\nAdd ability to duplicate a notebook', 'language_name': 'Python', 'license': 'bsd-3-clause', 'hunks': "@@ -0,0 +1,24 @@\n+import fs from 'fs';\n+import path from 'path';\n+import tmp from 'tmp';\n+\n+export function copyNotebook(filename) {\n+  return new Promise((resolve, reject) => {\n+    const readStream = fs.createReadStream(filename);\n+    readStream.on('error', reject);\n+\n+    const base = path.basename(filename, '.ipynb');\n+\n+    tmp.tmpName({ prefix: `${base}-Copy`, postfix: '.ipynb' }, (err, newFilename) => {\n+      if (err) {\n+        reject(err);\n+      }\n+\n+      const writeStream = fs.createWriteStream(newFilename);\n+      writeStream.on('error', reject);\n+\n+      readStream.pipe(writeStream);\n+      writeStream.on('close', resolve(newFilename));\n+    });\n+  });\n+}\n", 'addition_count': 24, 'deletion_count': 0, 'src_file': '/dev/null', 'tgt_file': 'b/src/notebook/utils/index.js', 'file_extension': '.js', 'before_file': 'ADDFILE', 'message': 'Merge pull request #530 from captainsafia/clone-notebook\n\nAdd ability to duplicate a notebook', 'author_name': 'GitHub', 'lang': 'js'}