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 @@ + xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"> - - - - - - - + + + + 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. +
+ +
+ +
+
+
StackEdit v4
+ Deprecated. +
+
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"