diff --git a/index.html b/index.html
index 962f580a..4ad2de16 100644
--- a/index.html
+++ b/index.html
@@ -3,6 +3,17 @@
StackEdit
+
+
+
+
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
index bfa2eaad..671f6a43 100644
--- a/index.js
+++ b/index.js
@@ -1,3 +1,5 @@
+process.env.NODE_ENV = 'production';
+
var cluster = require('cluster');
var http = require('http');
var https = require('https');
@@ -7,13 +9,13 @@ var app = express();
require('./server')(app);
-var port = process.env.PORT || 8080;
+var port = parseInt(process.env.PORT || 8080, 10);
if(port === 443) {
var fs = require('fs');
var credentials = {
- key: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.key'), 'utf8'),
- cert: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.crt'), 'utf8'),
- ca: fs.readFileSync(path.join(__dirname, '/../../shared/config/ssl.ca'), 'utf8').split('\n\n')
+ key: fs.readFileSync(path.join(__dirname, 'ssl.key'), 'utf8'),
+ cert: fs.readFileSync(path.join(__dirname, 'ssl.crt'), 'utf8'),
+ ca: fs.readFileSync(path.join(__dirname, 'ssl.ca'), 'utf8').split('\n\n')
};
var httpsServer = https.createServer(credentials, app);
httpsServer.listen(port, null, function() {
diff --git a/package.json b/package.json
index 5dd90046..b082fec7 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "stackedit",
- "version": "5.0.1",
+ "version": "5.0.2",
"description": "Free, open-source, full-featured Markdown editor",
"author": "Benoit Schweblin",
"license": "Apache-2.0",
@@ -39,7 +39,7 @@
"raw-loader": "^0.5.1",
"request": "^2.82.0",
"serve-static": "^1.12.6",
- "stackedit": "^4.3.16",
+ "stackedit": "^4.3.17",
"vue": "^2.3.3",
"vuex": "^2.3.1"
},
diff --git a/server/index.js b/server/index.js
index fcf40c65..e701d360 100644
--- a/server/index.js
+++ b/server/index.js
@@ -3,48 +3,56 @@ var serveStatic = require('serve-static');
var path = require('path');
module.exports = function (app) {
- // Force HTTPS on stackedit.io
- app.all('*', function(req, res, next) {
- if (req.headers.host === 'stackedit.io' && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
- return res.redirect('https://stackedit.io' + req.url);
- }
- /\.(eot|ttf|woff|svg)$/.test(req.url) && res.header('Access-Control-Allow-Origin', '*');
- next();
- });
-
// Use gzip compression
- app.use(compression());
+ if (process.env.NODE_ENV === 'production') {
+ // Force HTTPS on stackedit.io
+ app.all('*', function(req, res, next) {
+ if (req.headers.host === 'stackedit.io' && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
+ return res.redirect('https://stackedit.io' + req.url);
+ }
+ /\.(eot|ttf|woff|svg)$/.test(req.url) && res.header('Access-Control-Allow-Origin', '*');
+ next();
+ });
+
+ app.use(compression());
+ }
- app.post('/pdfExport', require('./pdf').export);
app.get('/oauth2/githubToken', require('./github').githubToken);
+ app.post('/pdfExport', require('stackedit/app/pdf').export);
+ app.post('/sshPublish', require('stackedit/app/ssh').publish);
+ app.post('/picasaImportImg', require('stackedit/app/picasa').importImg);
+ app.get('/downloadImport', require('stackedit/app/download').importPublic);
- // Serve landing.html in /
- app.get('/', function(req, res) {
- res.sendFile(require.resolve('stackedit/views/landing.html'));
- });
- // Serve editor.html in /viewer
- app.get('/editor', function(req, res) {
- res.sendFile(require.resolve('stackedit/views/editor.html'));
- });
- // Serve viewer.html in /viewer
- app.get('/viewer', function(req, res) {
- res.sendFile(require.resolve('stackedit/views/viewer.html'));
- });
- // Serve index.html in /app
- app.get('/app', function(req, res) {
- res.sendFile(path.join(__dirname, '../dist/index.html'));
- });
// Serve callback.html in /app
app.get('/oauth2/callback', function(req, res) {
- res.sendFile(path.join(__dirname, '../dist/static/oauth2/callback.html'));
+ res.sendFile(path.join(__dirname, '../static/oauth2/callback.html'));
});
// Serve static resources
- app.use(serveStatic(path.join(__dirname, '../dist'))); // v5
- app.use(serveStatic(path.dirname(require.resolve('stackedit/public/cache.manifest')))); // v4
+ if (process.env.NODE_ENV === 'production') {
+ // Serve landing.html in /
+ app.get('/', function(req, res) {
+ res.sendFile(require.resolve('stackedit/views/landing.html'));
+ });
+ // Serve editor.html in /viewer
+ app.get('/editor', function(req, res) {
+ res.sendFile(require.resolve('stackedit/views/editor.html'));
+ });
+ // Serve viewer.html in /viewer
+ app.get('/viewer', function(req, res) {
+ res.sendFile(require.resolve('stackedit/views/viewer.html'));
+ });
+ // Serve index.html in /app
+ app.get('/app', function(req, res) {
+ res.sendFile(path.join(__dirname, '../dist/index.html'));
+ });
- // Error 404
- app.use(function(req, res) {
- res.status(404).sendFile(require.resolve('stackedit/views/error_404.html'));
- });
+ app.use(serveStatic(path.join(__dirname, '../dist'))); // v5
+ app.use(serveStatic(path.dirname(require.resolve('stackedit/public/cache.manifest')))); // v4
+
+ // Error 404
+ app.use(function(req, res) {
+ res.status(404).sendFile(require.resolve('stackedit/views/error_404.html'));
+ });
+ }
};
diff --git a/src/assets/iconBlogger.svg b/src/assets/iconBlogger.svg
index 92b32835..1e01ac17 100644
--- a/src/assets/iconBlogger.svg
+++ b/src/assets/iconBlogger.svg
@@ -1,13 +1,10 @@
diff --git a/src/components/NavigationBar.vue b/src/components/NavigationBar.vue
index ccf7066b..d3903422 100644
--- a/src/components/NavigationBar.vue
+++ b/src/components/NavigationBar.vue
@@ -372,7 +372,7 @@ $t: 3000ms;
.navigation-bar__spinner {
width: 22px;
margin: 8px 0 0 8px;
- color: rgba(255, 255, 255, 0.67);
+ color: #b2b2b2;
.icon {
width: 22px;
diff --git a/src/components/menus/MoreMenu.vue b/src/components/menus/MoreMenu.vue
index 762a445c..161b0037 100644
--- a/src/components/menus/MoreMenu.vue
+++ b/src/components/menus/MoreMenu.vue
@@ -15,6 +15,16 @@
Reset application
Sign out and clean local data.
+
+
diff --git a/src/services/localDbSvc.js b/src/services/localDbSvc.js
index 5840d373..6bac8ad0 100644
--- a/src/services/localDbSvc.js
+++ b/src/services/localDbSvc.js
@@ -2,6 +2,7 @@ import 'babel-polyfill';
import 'indexeddbshim/dist/indexeddbshim';
import utils from './utils';
import store from '../store';
+import welcomeFile from '../data/welcomeFile.md';
const indexedDB = window.indexedDB;
const dbVersion = 1;
@@ -91,7 +92,7 @@ const contentTypes = {
syncedContent: true,
};
-export default {
+const localDbSvc = {
lastTx: 0,
hashMap,
connection: new Connection(),
@@ -297,3 +298,77 @@ export default {
}, () => store.dispatch('notification/error', 'Could not delete local database.'));
},
};
+
+const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
+ // Item does not exist, create it
+ .catch(() => store.commit(`${type}/setItem`, {
+ id: `${fileId}/${type}`,
+ }));
+localDbSvc.loadSyncedContent = loader('syncedContent');
+localDbSvc.loadContentState = loader('contentState');
+
+const ifNoId = cb => (obj) => {
+ if (obj.id) {
+ return obj;
+ }
+ return cb();
+};
+
+// Load the DB on boot
+localDbSvc.sync()
+ // And watch file changing
+ .then(() => store.watch(
+ () => store.getters['file/current'].id,
+ () => Promise.resolve(store.getters['file/current'])
+ // If current file has no ID, get the most recent file
+ .then(ifNoId(() => store.getters['file/lastOpened']))
+ // If still no ID, create a new file
+ .then(ifNoId(() => {
+ const id = utils.uid();
+ store.commit('content/setItem', {
+ id: `${id}/content`,
+ text: welcomeFile,
+ });
+ store.commit('file/setItem', {
+ id,
+ name: 'Welcome file',
+ });
+ return store.state.file.itemMap[id];
+ }))
+ .then((currentFile) => {
+ // Fix current file ID
+ if (store.getters['file/current'].id !== currentFile.id) {
+ store.commit('file/setCurrentId', currentFile.id);
+ // Wait for the next watch tick
+ return null;
+ }
+ return Promise.resolve()
+ // Load contentState from DB
+ .then(() => localDbSvc.loadContentState(currentFile.id))
+ // Load syncedContent from DB
+ .then(() => localDbSvc.loadSyncedContent(currentFile.id))
+ // Load content from DB
+ .then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
+ .then(
+ // Success, set last opened file
+ () => store.dispatch('data/setLastOpenedId', currentFile.id),
+ (err) => {
+ // Failure (content is not available), go back to previous file
+ const lastOpenedFile = store.getters['file/lastOpened'];
+ store.commit('file/setCurrentId', lastOpenedFile.id);
+ throw err;
+ },
+ );
+ })
+ .catch((err) => {
+ console.error(err); // eslint-disable-line no-console
+ store.dispatch('notification/error', err);
+ }),
+ {
+ immediate: true,
+ }));
+
+// Sync local DB periodically
+utils.setInterval(() => localDbSvc.sync(), 1000);
+
+export default localDbSvc;
diff --git a/src/services/providers/googleDriveAppDataProvider.js b/src/services/providers/googleDriveAppDataProvider.js
index 469ef74b..10d87b80 100644
--- a/src/services/providers/googleDriveAppDataProvider.js
+++ b/src/services/providers/googleDriveAppDataProvider.js
@@ -62,8 +62,7 @@ export default providerRegistry.register({
.then(() => syncData);
},
downloadContent(token, syncLocation) {
- return this.downloadData(token, `${syncLocation.fileId}/content`)
- .then(() => syncLocation);
+ return this.downloadData(token, `${syncLocation.fileId}/content`);
},
downloadData(token, dataId) {
const syncData = store.getters['data/syncDataByItemId'][dataId];
diff --git a/src/services/syncSvc.js b/src/services/syncSvc.js
index 6cb89f62..27559511 100644
--- a/src/services/syncSvc.js
+++ b/src/services/syncSvc.js
@@ -1,6 +1,5 @@
import localDbSvc from './localDbSvc';
import store from '../store';
-import welcomeFile from '../data/welcomeFile.md';
import utils from './utils';
import diffUtils from './diffUtils';
import providerRegistry from './providers/providerRegistry';
@@ -54,15 +53,6 @@ function cleanSyncedContent(syncedContent) {
});
}
-const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
- // Item does not exist, create it
- .catch(() => store.commit(`${type}/setItem`, {
- id: `${fileId}/${type}`,
- }));
-const loadContent = loader('content');
-const loadSyncedContent = loader('syncedContent');
-const loadContentState = loader('contentState');
-
function applyChanges(changes) {
const token = mainProvider.getToken();
const storeItemMap = { ...store.getters.allItemMap };
@@ -70,31 +60,31 @@ function applyChanges(changes) {
let syncDataChanged = false;
changes.forEach((change) => {
- const existingSyncData = syncData[change.fileId];
- const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
- if (change.removed && existingSyncData) {
- if (existingItem) {
- // Remove object from the store
- store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
- delete storeItemMap[existingItem.id];
- }
- delete syncData[change.fileId];
- syncDataChanged = true;
- } else if (!change.removed && change.item && change.item.hash && (
- // Ignore items that belong to another user (like settings)
- !change.item.sub || change.item.sub === token.sub
- )) {
- if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
- !existingItem || existingItem.hash !== change.item.hash
- ))) {
- // Put object in the store
- if (change.item.type !== 'content') { // Merge contents later
- store.commit(`${change.item.type}/setItem`, change.item);
- storeItemMap[change.item.id] = change.item;
+ // Ignore items that belong to another user (like settings)
+ if (!change.item || !change.item.sub || change.item.sub === token.sub) {
+ const existingSyncData = syncData[change.fileId];
+ const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
+ if (change.removed && existingSyncData) {
+ if (existingItem) {
+ // Remove object from the store
+ store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
+ delete storeItemMap[existingItem.id];
}
+ delete syncData[change.fileId];
+ syncDataChanged = true;
+ } else if (!change.removed && change.item && change.item.hash) {
+ if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
+ !existingItem || existingItem.hash !== change.item.hash
+ ))) {
+ // Put object in the store
+ if (change.item.type !== 'content') { // Merge contents later
+ store.commit(`${change.item.type}/setItem`, change.item);
+ storeItemMap[change.item.id] = change.item;
+ }
+ }
+ syncData[change.fileId] = change.syncData;
+ syncDataChanged = true;
}
- syncData[change.fileId] = change.syncData;
- syncDataChanged = true;
}
});
@@ -121,7 +111,7 @@ function createSyncLocation(syncLocation) {
...content,
history: [content.hash],
}, syncLocation)
- .then(syncLocationToStore => loadSyncedContent(fileId)
+ .then(syncLocationToStore => localDbSvc.loadSyncedContent(fileId)
.then(() => {
const newSyncedContent = utils.deepCopy(
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
@@ -137,9 +127,11 @@ function createSyncLocation(syncLocation) {
});
}
-function syncFile(fileId) {
- return loadSyncedContent(fileId)
- .then(() => loadContent(fileId))
+function syncFile(fileId, needSyncRestartParam = false) {
+ let needSyncRestart = needSyncRestartParam;
+ return localDbSvc.loadSyncedContent(fileId)
+ .then(() => localDbSvc.loadItem(`${fileId}/content`)
+ .catch(() => {})) // Item may not exist if content has not been downloaded yet
.then(() => {
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
@@ -176,6 +168,9 @@ function syncFile(fileId) {
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
let mergedContent = (() => {
const clientContent = utils.deepCopy(getContent());
+ if (!clientContent) {
+ return utils.deepCopy(serverContent);
+ }
if (!serverContent) {
// Sync location has not been created yet
return clientContent;
@@ -200,10 +195,17 @@ function syncFile(fileId) {
return diffUtils.mergeContent(serverContent, clientContent, lastMergedContent);
})();
- // Update content in store
- store.commit('content/patchItem', {
+ if (!mergedContent) {
+ errorLocations[syncLocation.id] = true;
+ return null;
+ }
+
+ // Update or set content in store
+ delete mergedContent.history;
+ store.commit('content/setItem', {
id: `${fileId}/content`,
...mergedContent,
+ hash: 0,
});
// Retrieve content with new `hash` and freeze it
@@ -272,6 +274,12 @@ function syncFile(fileId) {
) {
store.commit('syncLocation/patchItem', syncLocationToStore);
}
+
+ // If content was just created, restart sync to create the file as well
+ const syncDataByItemId = store.getters['data/syncDataByItemId'];
+ if (!syncDataByItemId[fileId]) {
+ needSyncRestart = true;
+ }
});
})
.catch((err) => {
@@ -298,13 +306,15 @@ function syncFile(fileId) {
.then(() => {
throw err;
}))
- .catch((err) => {
- if (err && err.message === 'TOO_LATE') {
- // Restart sync
- return syncFile(fileId);
- }
- throw err;
- });
+ .then(
+ () => needSyncRestart,
+ (err) => {
+ if (err && err.message === 'TOO_LATE') {
+ // Restart sync
+ return syncFile(fileId, needSyncRestart);
+ }
+ throw err;
+ });
}
@@ -320,7 +330,10 @@ function syncDataItem(dataId) {
.then((serverItem = null) => {
const dataSyncData = store.getters['data/dataSyncData'][dataId];
let mergedItem = (() => {
- const clientItem = utils.deepCopy(store.getters[`data/${dataId}`]);
+ const clientItem = utils.deepCopy(store.state.data.itemMap[dataId]);
+ if (!clientItem) {
+ return serverItem;
+ }
if (!serverItem) {
return clientItem;
}
@@ -341,6 +354,10 @@ function syncDataItem(dataId) {
return clientItem;
})();
+ if (!mergedItem) {
+ return null;
+ }
+
// Update item in store
store.commit('data/setItem', {
id: dataId,
@@ -401,7 +418,10 @@ function sync() {
Object.keys(storeItemMap).some((id) => {
const item = storeItemMap[id];
const existingSyncData = syncDataByItemId[id];
- if (!existingSyncData || existingSyncData.hash !== item.hash) {
+ if ((!existingSyncData || existingSyncData.hash !== item.hash) &&
+ // Add file if content has been uploaded
+ (item.type !== 'file' || syncDataByItemId[`${id}/content`])
+ ) {
result = mainProvider.saveItem(
mainToken,
// Use deepCopy to freeze objects
@@ -454,9 +474,12 @@ function sync() {
});
const getOneFileIdToSync = () => {
- const allContentIds = Object.keys(localDbSvc.hashMap.content);
+ const contentIds = [...new Set([
+ ...Object.keys(localDbSvc.hashMap.content),
+ ...store.getters['file/items'].map(file => `${file.id}/content`),
+ ])];
let fileId;
- allContentIds.some((contentId) => {
+ contentIds.some((contentId) => {
// Get content hash from itemMap or from localDbSvc if not loaded
const loadedContent = store.state.content.itemMap[contentId];
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
@@ -470,10 +493,13 @@ function sync() {
return fileId;
};
- const syncNextFile = () => {
+ const syncNextFile = (needSyncRestartParam) => {
const fileId = getOneFileIdToSync();
- return fileId && syncFile(fileId)
- .then(() => syncNextFile());
+ if (!fileId) {
+ return needSyncRestartParam;
+ }
+ return syncFile(fileId, needSyncRestartParam)
+ .then(needSyncRestart => syncNextFile(needSyncRestart));
};
return Promise.resolve()
@@ -482,21 +508,29 @@ function sync() {
.then(() => syncDataItem('settings'))
.then(() => syncDataItem('templates'))
.then(() => {
- const currentFileId = store.getters['content/current'].id;
+ const currentFileId = store.getters['file/current'].id;
if (currentFileId) {
// Sync current file first
return syncFile(currentFileId)
- .then(() => syncNextFile());
+ .then(needSyncRestart => syncNextFile(needSyncRestart));
}
return syncNextFile();
})
- .catch((err) => {
- if (err && err.message === 'TOO_LATE') {
- // Restart sync
- return sync();
- }
- throw err;
- });
+ .then(
+ (needSyncRestart) => {
+ if (needSyncRestart) {
+ // Restart sync
+ return sync();
+ }
+ return null;
+ },
+ (err) => {
+ if (err && err.message === 'TOO_LATE') {
+ // Restart sync
+ return sync();
+ }
+ throw err;
+ });
});
}
@@ -552,58 +586,6 @@ utils.setInterval(() => {
}
}, 1000);
-const ifNoId = cb => (obj) => {
- if (obj.id) {
- return obj;
- }
- return cb();
-};
-
-// Load the DB on boot
-localDbSvc.sync()
- // And watch file changing
- .then(() => store.watch(
- () => store.getters['file/current'].id,
- () => Promise.resolve(store.getters['file/current'])
- // If current file has no ID, get the most recent file
- .then(ifNoId(() => store.getters['file/lastOpened']))
- // If still no ID, create a new file
- .then(ifNoId(() => {
- const id = utils.uid();
- store.commit('content/setItem', {
- id: `${id}/content`,
- text: welcomeFile,
- });
- store.commit('file/setItem', {
- id,
- name: 'Welcome file',
- });
- return store.state.file.itemMap[id];
- }))
- .then((currentFile) => {
- // Fix current file ID
- if (store.getters['file/current'].id !== currentFile.id) {
- store.commit('file/setCurrentId', currentFile.id);
- // Wait for the next watch tick
- return null;
- }
- // Set last opened
- store.dispatch('data/setLastOpenedId', currentFile.id);
- return Promise.resolve()
- // Load contentState from DB
- .then(() => loadContentState(currentFile.id))
- // Load syncedContent from DB
- .then(() => loadSyncedContent(currentFile.id))
- // Load content from DB
- .then(() => localDbSvc.loadItem(`${currentFile.id}/content`));
- }),
- {
- immediate: true,
- }));
-
-// Sync local DB periodically
-utils.setInterval(() => localDbSvc.sync(), 1000);
-
// Unload contents from memory periodically
utils.setInterval(() => {
// Wait for sync and publish to finish
diff --git a/yarn.lock b/yarn.lock
index 6e909fe5..7001b307 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5945,9 +5945,9 @@ sshpk@^1.7.0:
jsbn "~0.1.0"
tweetnacl "~0.14.0"
-stackedit@^4.3.16:
- version "4.3.16"
- resolved "https://registry.yarnpkg.com/stackedit/-/stackedit-4.3.16.tgz#e4df6af29e70d2c45a547523e6aa4f8025c1556d"
+stackedit@^4.3.17:
+ version "4.3.17"
+ resolved "https://registry.yarnpkg.com/stackedit/-/stackedit-4.3.17.tgz#3c524a625f7399d13b06706da2305a131a8df56c"
dependencies:
bower "^1.8.2"
compression "^1.0.11"