Skip to content

Commit

Permalink
feat: allow a user to sync a space with the remote
Browse files Browse the repository at this point in the history
closes #114
  • Loading branch information
juancarlosfarah committed Jun 21, 2019
1 parent 33fc4a5 commit 2e468fa
Show file tree
Hide file tree
Showing 17 changed files with 350 additions and 132 deletions.
6 changes: 5 additions & 1 deletion public/app/config/channels.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ module.exports = {
EXPORTED_SPACE_CHANNEL: 'space:exported',
SHOW_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:show',
SHOW_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:show',
SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show',
RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:response',
RESPOND_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:respond',
SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show',
RESPOND_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:respond',
GET_USER_FOLDER_CHANNEL: 'user:folder:get',
GET_LANGUAGE_CHANNEL: 'user:lang:get',
Expand All @@ -31,4 +31,8 @@ module.exports = {
PATCH_APP_INSTANCE_CHANNEL: 'app-instance:patch',
GET_DATABASE_CHANNEL: 'developer:database:get',
SET_DATABASE_CHANNEL: 'developer:database:set',
SHOW_SYNC_SPACE_PROMPT_CHANNEL: 'prompt:space:sync:show',
RESPOND_SYNC_SPACE_PROMPT_CHANNEL: 'prompt:space:sync:respond',
SYNC_SPACE_CHANNEL: 'space:sync',
SYNCED_SPACE_CHANNEL: 'space:synced',
};
4 changes: 2 additions & 2 deletions public/app/config/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ const APPLICATION = 'Application';

const VAR_FOLDER = `${app.getPath('userData')}/var`;
const DATABASE_PATH = `${VAR_FOLDER}/db.json`;
const TEMPORARY_EXTRACT_FOLDER = 'tmp';
const TMP_FOLDER = 'tmp';
const DEFAULT_LANG = 'en';
const DEFAULT_DEVELOPER_MODE = false;

module.exports = {
DEFAULT_DEVELOPER_MODE,
DOWNLOADABLE_MIME_TYPES,
TEMPORARY_EXTRACT_FOLDER,
TMP_FOLDER,
RESOURCE,
APPLICATION,
DATABASE_PATH,
Expand Down
4 changes: 4 additions & 0 deletions public/app/config/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const ERROR_SETTING_DEVELOPER_MODE =
'There was an error setting the developer mode';
const ERROR_GETTING_DATABASE = 'There was an error getting the database.';
const ERROR_SETTING_DATABASE = 'There was an error updating the database.';
const SUCCESS_SYNCING_MESSAGE = 'Space was successfully synced';
const ERROR_SYNCING_MESSAGE = 'There was an error syncing the space.';

module.exports = {
ERROR_GETTING_DEVELOPER_MODE,
Expand Down Expand Up @@ -65,4 +67,6 @@ module.exports = {
INVALID_SPACE_ID,
ERROR_GETTING_DATABASE,
ERROR_SETTING_DATABASE,
SUCCESS_SYNCING_MESSAGE,
ERROR_SYNCING_MESSAGE,
};
107 changes: 107 additions & 0 deletions public/app/download.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
const request = require('request-promise');
const cheerio = require('cheerio');
const download = require('download');
const providers = require('./config/providers');
const logger = require('./logger');
const mapping = require('./config/mapping');
const {
getExtension,
isDownloadable,
generateHash,
isFileAvailable,
} = require('./utilities');

const getDownloadUrl = async ({ url, lang }) => {
let proxied = false;
Expand All @@ -22,6 +31,104 @@ const getDownloadUrl = async ({ url, lang }) => {
return false;
};

const downloadSpaceResources = async ({ lang, space, absoluteSpacePath }) => {
// make a working copy of the space to save
const spaceToSave = { ...space };

const { phases, image, id } = spaceToSave;
const relativeSpacePath = id;

// if there is a background/thumbnail image, save it
if (image) {
const { thumbnailUrl, backgroundUrl } = image;
const assets = [
{ url: thumbnailUrl, key: 'thumbnailAsset' },
{ url: backgroundUrl, key: 'backgroundAsset' },
];

// eslint-disable-next-line no-restricted-syntax
for (const asset of assets) {
let { url } = asset;
const { key } = asset;
if (url) {
// default to https
if (url.startsWith('//')) {
url = `https:${url}`;
}
const ext = getExtension({ url });
const hash = generateHash({ url });
const imageFileName = `${hash}.${ext}`;
const relativeImagePath = `${relativeSpacePath}/${imageFileName}`;
const absoluteImagePath = `${absoluteSpacePath}/${imageFileName}`;
// eslint-disable-next-line no-await-in-loop
const imageAvailable = await isFileAvailable(absoluteImagePath);
if (!imageAvailable) {
logger.debug(`downloading ${url}`);
// eslint-disable-next-line no-await-in-loop
await download(url, absoluteSpacePath, {
filename: imageFileName,
});
logger.debug(
`downloaded ${url} to ${absoluteSpacePath}/${imageFileName}`
);
}
spaceToSave.image[key] = relativeImagePath;
}
}
}
// eslint-disable-next-line no-restricted-syntax
for (const phase of phases) {
const { items = [] } = phase;
for (let i = 0; i < items.length; i += 1) {
const resource = items[i];
if (resource && isDownloadable(resource)) {
let { url } = resource;

// check mappings for files
if (mapping[url]) {
url = mapping[url];
}

// download from proxy url if available
// eslint-disable-next-line no-await-in-loop
const downloadUrl = await getDownloadUrl({ url, lang });
if (downloadUrl) {
url = downloadUrl;
}

// default to https
if (url.startsWith('//')) {
url = `https:${url}`;
}

// generate hash and get extension to save file
const hash = generateHash(resource);
const ext = getExtension(resource);
const fileName = `${hash}.${ext}`;
const relativeFilePath = `${relativeSpacePath}/${fileName}`;
const absoluteFilePath = `${absoluteSpacePath}/${fileName}`;
phase.items[i].hash = hash;

// eslint-disable-next-line no-await-in-loop
const fileAvailable = await isFileAvailable(absoluteFilePath);

// if the file is available, point this resource to its path
if (!fileAvailable) {
logger.debug(`downloading ${url}`);
// eslint-disable-next-line no-await-in-loop
await download(url, absoluteSpacePath, {
filename: fileName,
});
logger.debug(`downloaded ${url} to ${absoluteSpacePath}/${fileName}`);
}
phase.items[i].asset = relativeFilePath;
}
}
}
return spaceToSave;
};

module.exports = {
getDownloadUrl,
downloadSpaceResources,
};
4 changes: 4 additions & 0 deletions public/app/listeners/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
const loadSpace = require('./loadSpace');
const saveSpace = require('./saveSpace');
const getSpace = require('./getSpace');
const showSyncSpacePrompt = require('./showSyncSpacePrompt');
const syncSpace = require('./syncSpace');

module.exports = {
loadSpace,
saveSpace,
getSpace,
showSyncSpacePrompt,
syncSpace,
};
4 changes: 2 additions & 2 deletions public/app/listeners/loadSpace.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const extract = require('extract-zip');
const rimraf = require('rimraf');
const fs = require('fs');
const { VAR_FOLDER, TEMPORARY_EXTRACT_FOLDER } = require('../config/config');
const { VAR_FOLDER, TMP_FOLDER } = require('../config/config');
const { LOADED_SPACE_CHANNEL } = require('../config/channels');
const {
ERROR_SPACE_ALREADY_AVAILABLE,
Expand All @@ -16,7 +16,7 @@ const { SPACES_COLLECTION } = require('../db');
const fsPromises = fs.promises;

const loadSpace = (mainWindow, db) => async (event, { fileLocation }) => {
const extractPath = `${VAR_FOLDER}/${TEMPORARY_EXTRACT_FOLDER}`;
const extractPath = `${VAR_FOLDER}/${TMP_FOLDER}`;
try {
extract(fileLocation, { dir: extractPath }, async extractError => {
if (extractError) {
Expand Down
119 changes: 10 additions & 109 deletions public/app/listeners/saveSpace.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
const isOnline = require('is-online');
const download = require('download');
const { VAR_FOLDER, DEFAULT_LANG } = require('../config/config');
const { getDownloadUrl } = require('../download');
const { SAVE_SPACE_CHANNEL } = require('../config/channels');
const {
ERROR_SPACE_ALREADY_AVAILABLE,
ERROR_DOWNLOADING_FILE,
} = require('../config/errors');
const logger = require('../logger');
const mapping = require('../config/mapping');
const {
isFileAvailable,
getExtension,
isDownloadable,
generateHash,
createSpaceDirectory,
} = require('../utilities');
const { DEFAULT_LANG } = require('../config/config');
const { createSpaceDirectory } = require('../utilities');
const { downloadSpaceResources } = require('../download');
const { SPACES_COLLECTION } = require('../db');

const saveSpace = (mainWindow, db) => async (event, { space }) => {
logger.debug('saving space');
// make a working copy of the space to save
const spaceToSave = { ...space };

try {
// get handle to spaces collection
const spaces = db.get(SPACES_COLLECTION);
Expand All @@ -45,109 +36,19 @@ const saveSpace = (mainWindow, db) => async (event, { space }) => {
}

// create directory where resources will be stored
createSpaceDirectory({ id });

const { phases, image } = spaceToSave;

const spacePath = id;
const absoluteSpacePath = createSpaceDirectory({ id });

// use language defined in space otherwise fall back on
// user language, otherwise fall back on the global default
const userLang = db.get('user.lang').value();
const lang = language || userLang || DEFAULT_LANG;

// todo: follow new format
// if there is a background/thumbnail image, save it
if (image) {
const { thumbnailUrl, backgroundUrl } = image;
const assets = [
{ url: thumbnailUrl, key: 'thumbnailAsset' },
{ url: backgroundUrl, key: 'backgroundAsset' },
];

// eslint-disable-next-line no-restricted-syntax
for (const asset of assets) {
let { url } = asset;
const { key } = asset;
if (url) {
// default to https
if (url.startsWith('//')) {
url = `https:${url}`;
}
const ext = getExtension({ url });
const hash = generateHash({ url });
const imageFileName = `${hash}.${ext}`;
const imagePath = `${spacePath}/${imageFileName}`;
const absoluteSpacePath = `${VAR_FOLDER}/${spacePath}`;
const absoluteImagePath = `${VAR_FOLDER}/${imagePath}`;
// eslint-disable-next-line no-await-in-loop
const imageAvailable = await isFileAvailable(absoluteImagePath);
if (!imageAvailable) {
logger.debug(`downloading ${url}`);
// eslint-disable-next-line no-await-in-loop
await download(url, absoluteSpacePath, {
filename: imageFileName,
});
logger.debug(
`downloaded ${url} to ${absoluteSpacePath}/${imageFileName}`
);
}
spaceToSave.image[key] = imagePath;
}
}
}
// eslint-disable-next-line no-restricted-syntax
for (const phase of phases) {
const { items = [] } = phase;
for (let i = 0; i < items.length; i += 1) {
const resource = items[i];
if (resource && isDownloadable(resource)) {
let { url } = resource;

// check mappings for files
if (mapping[url]) {
url = mapping[url];
}

// download from proxy url if available
// eslint-disable-next-line no-await-in-loop
const downloadUrl = await getDownloadUrl({ url, lang });
if (downloadUrl) {
url = downloadUrl;
}

// default to https
if (url.startsWith('//')) {
url = `https:${url}`;
}
const spaceToSave = await downloadSpaceResources({
lang,
space,
absoluteSpacePath,
});

// generate hash and get extension to save file
const hash = generateHash(resource);
const ext = getExtension(resource);
const fileName = `${hash}.${ext}`;
const filePath = `${spacePath}/${fileName}`;
const absoluteSpacePath = `${VAR_FOLDER}/${spacePath}`;
const absoluteFilePath = `${VAR_FOLDER}/${filePath}`;
phase.items[i].hash = hash;

// eslint-disable-next-line no-await-in-loop
const fileAvailable = await isFileAvailable(absoluteFilePath);

// if the file is available, point this resource to its path
if (!fileAvailable) {
logger.debug(`downloading ${url}`);
// eslint-disable-next-line no-await-in-loop
await download(url, absoluteSpacePath, {
filename: fileName,
});
logger.debug(
`downloaded ${url} to ${absoluteSpacePath}/${fileName}`
);
}
phase.items[i].asset = filePath;
}
}
}
// mark space as saved
spaceToSave.saved = true;
spaces.push(spaceToSave).write();
Expand Down
21 changes: 21 additions & 0 deletions public/app/listeners/showSyncSpacePrompt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const {
dialog,
// eslint-disable-next-line import/no-extraneous-dependencies
} = require('electron');
const { RESPOND_SYNC_SPACE_PROMPT_CHANNEL } = require('../config/channels');

const showSyncSpacePrompt = mainWindow => () => {
const options = {
type: 'warning',
buttons: ['Cancel', 'Sync'],
defaultId: 0,
cancelId: 0,
message:
'Are you sure you want to sync this space? All user input will be deleted.',
};
dialog.showMessageBox(null, options, respond => {
mainWindow.webContents.send(RESPOND_SYNC_SPACE_PROMPT_CHANNEL, respond);
});
};

module.exports = showSyncSpacePrompt;
Loading

0 comments on commit 2e468fa

Please sign in to comment.