Stackedit/src/services/providers/googleDriveWorkspaceProvider.js

533 lines
16 KiB
JavaScript
Raw Normal View History

2017-12-10 23:49:20 +00:00
import store from '../../store';
import googleHelper from './helpers/googleHelper';
2018-04-27 14:37:05 +00:00
import Provider from './common/Provider';
2017-12-10 23:49:20 +00:00
import utils from '../utils';
import workspaceSvc from '../workspaceSvc';
2017-12-10 23:49:20 +00:00
2018-04-27 14:37:05 +00:00
let fileIdToOpen;
let syncStartPageToken;
export default new Provider({
2017-12-10 23:49:20 +00:00
id: 'googleDriveWorkspace',
2018-07-17 19:58:40 +00:00
name: 'Google Drive',
2017-12-10 23:49:20 +00:00
getToken() {
2017-12-17 15:08:52 +00:00
return store.getters['workspace/syncToken'];
2017-12-10 23:49:20 +00:00
},
getWorkspaceParams({ folderId }) {
return {
2017-12-17 15:08:52 +00:00
providerId: this.id,
folderId,
};
},
getWorkspaceLocationUrl({ folderId }) {
return `https://docs.google.com/folder/d/${folderId}`;
},
getSyncDataUrl({ id }) {
return `https://docs.google.com/file/d/${id}/edit`;
},
getSyncDataDescription({ id }) {
return id;
},
async initWorkspace() {
2018-05-06 00:46:33 +00:00
const makeWorkspaceId = folderId => folderId
&& utils.makeWorkspaceId(this.getWorkspaceParams({ folderId }));
2017-12-17 15:08:52 +00:00
2018-01-04 20:19:10 +00:00
const getWorkspace = folderId =>
store.getters['workspace/workspacesById'][makeWorkspaceId(folderId)];
2017-12-17 15:08:52 +00:00
2018-05-13 13:27:33 +00:00
const initFolder = async (token, folder) => {
const appProperties = {
folderId: folder.id,
dataFolderId: folder.appProperties.dataFolderId,
trashFolderId: folder.appProperties.trashFolderId,
};
// Make sure data folder exists
if (!appProperties.dataFolderId) {
const dataFolder = await googleHelper.uploadFile({
2017-12-10 23:49:20 +00:00
token,
2018-05-13 13:27:33 +00:00
name: '.stackedit-data',
parents: [folder.id],
appProperties: { folderId: folder.id },
mediaType: googleHelper.folderMimeType,
});
appProperties.dataFolderId = dataFolder.id;
2018-05-13 13:27:33 +00:00
}
// Make sure trash folder exists
if (!appProperties.trashFolderId) {
const trashFolder = await googleHelper.uploadFile({
2017-12-10 23:49:20 +00:00
token,
2018-05-13 13:27:33 +00:00
name: '.stackedit-trash',
parents: [folder.id],
appProperties: { folderId: folder.id },
mediaType: googleHelper.folderMimeType,
});
appProperties.trashFolderId = trashFolder.id;
2018-05-13 13:27:33 +00:00
}
// Update workspace if some properties are missing
if (appProperties.folderId !== folder.appProperties.folderId
|| appProperties.dataFolderId !== folder.appProperties.dataFolderId
|| appProperties.trashFolderId !== folder.appProperties.trashFolderId
) {
await googleHelper.uploadFile({
2017-12-10 23:49:20 +00:00
token,
2018-05-13 13:27:33 +00:00
appProperties,
mediaType: googleHelper.folderMimeType,
fileId: folder.id,
2017-12-10 23:49:20 +00:00
});
2018-05-13 13:27:33 +00:00
}
2017-12-17 15:08:52 +00:00
2018-05-13 13:27:33 +00:00
// Update workspace in the store
const workspaceId = makeWorkspaceId(folder.id);
store.dispatch('workspace/patchWorkspacesById', {
2018-05-13 13:27:33 +00:00
[workspaceId]: {
id: workspaceId,
sub: token.sub,
name: folder.name,
providerId: this.id,
folderId: folder.id,
teamDriveId: folder.teamDriveId,
dataFolderId: appProperties.dataFolderId,
trashFolderId: appProperties.trashFolderId,
},
2017-12-10 23:49:20 +00:00
});
2018-05-13 13:27:33 +00:00
};
2017-12-10 23:49:20 +00:00
2018-05-13 13:27:33 +00:00
// Token sub is in the workspace or in the url if workspace is about to be created
const { sub } = getWorkspace(utils.queryParams.folderId) || utils.queryParams;
// See if we already have a token
2018-06-21 19:16:33 +00:00
let token = store.getters['data/googleTokensBySub'][sub];
2018-05-13 13:27:33 +00:00
// If no token has been found, popup an authorize window and get one
if (!token || !token.isDrive || !token.driveFullAccess) {
2018-06-07 23:56:11 +00:00
await store.dispatch('modal/open', 'workspaceGoogleRedirection');
2018-05-13 13:27:33 +00:00
token = await googleHelper.addDriveAccount(true, utils.queryParams.sub);
}
let { folderId } = utils.queryParams;
// If no folderId is provided, create one
if (!folderId) {
const folder = await googleHelper.uploadFile({
token,
name: 'StackEdit workspace',
parents: [],
mediaType: googleHelper.folderMimeType,
});
await initFolder(token, {
...folder,
appProperties: {},
});
folderId = folder.id;
}
// Init workspace
if (!getWorkspace(folderId)) {
2018-05-13 13:27:33 +00:00
let folder;
try {
folder = await googleHelper.getFile(token, folderId);
2018-05-13 13:27:33 +00:00
} catch (err) {
throw new Error(`Folder ${folderId} is not accessible. Make sure you have the right permissions.`);
}
folder.appProperties = folder.appProperties || {};
const folderIdProperty = folder.appProperties.folderId;
if (folderIdProperty && folderIdProperty !== folderId) {
throw new Error(`Folder ${folderId} is part of another workspace.`);
}
await initFolder(token, folder);
}
return getWorkspace(folderId);
2018-01-04 20:19:10 +00:00
},
2018-05-13 13:27:33 +00:00
async performAction() {
const state = googleHelper.driveState || {};
const token = this.getToken();
switch (token && state.action) {
case 'create': {
const driveFolder = googleHelper.driveActionFolder;
2018-06-21 19:16:33 +00:00
let syncData = store.getters['data/syncDataById'][driveFolder.id];
2018-05-13 13:27:33 +00:00
if (!syncData && driveFolder.appProperties.id) {
// Create folder if not already synced
store.commit('folder/setItem', {
id: driveFolder.appProperties.id,
name: driveFolder.name,
});
2018-06-21 19:16:33 +00:00
const item = store.state.folder.itemsById[driveFolder.appProperties.id];
2018-05-13 13:27:33 +00:00
syncData = {
id: driveFolder.id,
itemId: item.id,
type: item.type,
hash: item.hash,
};
2018-06-21 19:16:33 +00:00
store.dispatch('data/patchSyncDataById', {
2018-05-13 13:27:33 +00:00
[syncData.id]: syncData,
});
2018-01-04 20:19:10 +00:00
}
const file = await workspaceSvc.createFile({
2018-05-13 13:27:33 +00:00
parentId: syncData && syncData.itemId,
}, true);
store.commit('file/setCurrentId', file.id);
// File will be created on next workspace sync
break;
}
case 'open': {
// open first file only
const firstFile = googleHelper.driveActionFiles[0];
2018-06-21 19:16:33 +00:00
const syncData = store.getters['data/syncDataById'][firstFile.id];
2018-05-13 13:27:33 +00:00
if (!syncData) {
fileIdToOpen = firstFile.id;
} else {
store.commit('file/setCurrentId', syncData.itemId);
}
break;
}
default:
}
2017-12-10 23:49:20 +00:00
},
2018-05-13 13:27:33 +00:00
async getChanges() {
2017-12-23 18:25:14 +00:00
const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken'];
2018-05-13 13:27:33 +00:00
const lastStartPageToken = store.getters['data/localSettings'].syncStartPageToken;
const { changes, startPageToken } = await googleHelper
.getChanges(syncToken, lastStartPageToken, false, workspace.teamDriveId);
2017-12-23 18:25:14 +00:00
2018-06-21 19:16:33 +00:00
syncStartPageToken = startPageToken;
return changes;
},
prepareChanges(changes) {
2018-05-13 13:27:33 +00:00
// Collect possible parent IDs
const parentIds = {};
Object.entries(store.getters['data/syncDataByItemId']).forEach(([id, syncData]) => {
parentIds[syncData.id] = id;
});
changes.forEach((change) => {
const { id } = (change.file || {}).appProperties || {};
if (id) {
parentIds[change.fileId] = id;
}
});
2017-12-23 18:25:14 +00:00
2018-05-13 13:27:33 +00:00
// Collect changes
2018-06-21 19:16:33 +00:00
const workspace = store.getters['workspace/currentWorkspace'];
2018-05-13 13:27:33 +00:00
const result = [];
changes.forEach((change) => {
// Ignore changes on StackEdit own folders
if (change.fileId === workspace.folderId
|| change.fileId === workspace.dataFolderId
|| change.fileId === workspace.trashFolderId
) {
return;
}
2017-12-23 18:25:14 +00:00
2018-05-13 13:27:33 +00:00
let contentChange;
if (change.file) {
// Ignore changes in files that are not in the workspace
const { appProperties } = change.file;
if (!appProperties || appProperties.folderId !== workspace.folderId
) {
return;
}
2017-12-23 18:25:14 +00:00
2018-05-13 13:27:33 +00:00
// If change is on a data item
if (change.file.parents[0] === workspace.dataFolderId) {
// Data item has a JSON filename
try {
change.item = JSON.parse(change.file.name);
} catch (e) {
return;
}
} else {
// Change on a file or folder
const type = change.file.mimeType === googleHelper.folderMimeType
? 'folder'
: 'file';
const item = {
id: appProperties.id,
type,
name: change.file.name,
parentId: null,
};
2017-12-23 18:25:14 +00:00
2018-05-13 13:27:33 +00:00
// Fill parentId
if (change.file.parents.some(parentId => parentId === workspace.trashFolderId)) {
item.parentId = 'trash';
} else {
change.file.parents.some((parentId) => {
if (!parentIds[parentId]) {
return false;
2017-12-23 18:25:14 +00:00
}
2018-05-13 13:27:33 +00:00
item.parentId = parentIds[parentId];
return true;
});
}
change.item = utils.addItemHash(item);
2017-12-23 18:25:14 +00:00
2018-05-13 13:27:33 +00:00
if (type === 'file') {
// create a fake change as a file content change
2018-06-21 19:16:33 +00:00
const id = `${appProperties.id}/content`;
const syncDataId = `${change.fileId}/content`;
2018-05-13 13:27:33 +00:00
contentChange = {
item: {
2018-06-21 19:16:33 +00:00
id,
2018-05-13 13:27:33 +00:00
type: 'content',
// Need a truthy value to force saving sync data
hash: 1,
},
syncData: {
2018-06-21 19:16:33 +00:00
id: syncDataId,
itemId: id,
2018-05-13 13:27:33 +00:00
type: 'content',
// Need a truthy value to force downloading the content
hash: 1,
},
2018-06-21 19:16:33 +00:00
syncDataId,
2017-12-10 23:49:20 +00:00
};
}
2018-05-13 13:27:33 +00:00
}
2017-12-23 18:25:14 +00:00
2018-05-13 13:27:33 +00:00
// Build sync data
change.syncData = {
id: change.fileId,
parentIds: change.file.parents,
itemId: change.item.id,
type: change.item.type,
hash: change.item.hash,
};
} else {
// Item was removed
2018-06-21 19:16:33 +00:00
const syncData = store.getters['data/syncDataById'][change.fileId];
2018-05-13 13:27:33 +00:00
if (syncData && syncData.type === 'file') {
// create a fake change as a file content change
contentChange = {
syncDataId: `${change.fileId}/content`,
};
}
}
// Push change
change.syncDataId = change.fileId;
result.push(change);
if (contentChange) {
result.push(contentChange);
}
});
2018-06-21 19:16:33 +00:00
2018-05-13 13:27:33 +00:00
return result;
2017-12-10 23:49:20 +00:00
},
2018-04-27 14:37:05 +00:00
onChangesApplied() {
2017-12-23 18:25:14 +00:00
store.dispatch('data/patchLocalSettings', {
2018-04-27 14:37:05 +00:00
syncStartPageToken,
2017-12-23 18:25:14 +00:00
});
},
2018-06-21 19:16:33 +00:00
async saveWorkspaceItem({ item, syncData, ifNotTooLate }) {
2018-05-13 13:27:33 +00:00
const workspace = store.getters['workspace/currentWorkspace'];
const syncToken = store.getters['workspace/syncToken'];
let file;
if (item.type !== 'file' && item.type !== 'folder') {
// For sync/publish locations, store item as filename
file = await googleHelper.uploadFile({
token: syncToken,
name: JSON.stringify(item),
parents: [workspace.dataFolderId],
appProperties: {
folderId: workspace.folderId,
},
fileId: syncData && syncData.id,
oldParents: syncData && syncData.parentIds,
ifNotTooLate,
});
} else {
// For type `file` or `folder`
const parentSyncData = store.getters['data/syncDataByItemId'][item.parentId];
let parentId;
if (item.parentId === 'trash') {
parentId = workspace.trashFolderId;
} else if (parentSyncData) {
parentId = parentSyncData.id;
} else {
parentId = workspace.folderId;
}
2018-04-11 15:19:20 +00:00
2018-05-13 13:27:33 +00:00
file = await googleHelper.uploadFile({
token: syncToken,
name: item.name,
parents: [parentId],
appProperties: {
id: item.id,
folderId: workspace.folderId,
},
mediaType: item.type === 'folder' ? googleHelper.folderMimeType : undefined,
fileId: syncData && syncData.id,
oldParents: syncData && syncData.parentIds,
ifNotTooLate,
});
}
2018-07-17 19:58:40 +00:00
// Build sync data to save
2018-05-13 13:27:33 +00:00
return {
2018-07-17 19:58:40 +00:00
syncData: {
id: file.id,
parentIds: file.parents,
itemId: item.id,
type: item.type,
hash: item.hash,
},
2018-05-13 13:27:33 +00:00
};
2017-12-23 18:25:14 +00:00
},
2018-06-21 19:16:33 +00:00
async removeWorkspaceItem({ syncData, ifNotTooLate }) {
2017-12-23 18:25:14 +00:00
// Ignore content deletion
2018-05-13 13:27:33 +00:00
if (syncData.type !== 'content') {
const syncToken = store.getters['workspace/syncToken'];
await googleHelper.removeFile(syncToken, syncData.id, ifNotTooLate);
2017-12-23 18:25:14 +00:00
}
},
2018-06-21 19:16:33 +00:00
async downloadWorkspaceContent({ token, contentSyncData, fileSyncData }) {
const data = await googleHelper.downloadFile(token, fileSyncData.id);
const content = Provider.parseContent(data, contentSyncData.itemId);
2018-05-13 13:27:33 +00:00
// Open the file requested by action if it wasn't synced yet
2018-06-21 19:16:33 +00:00
if (fileIdToOpen && fileIdToOpen === fileSyncData.id) {
2018-05-13 13:27:33 +00:00
fileIdToOpen = null;
// Open the file once downloaded content has been stored
setTimeout(() => {
2018-06-21 19:16:33 +00:00
store.commit('file/setCurrentId', fileSyncData.itemId);
2018-05-13 13:27:33 +00:00
}, 10);
}
2018-06-07 23:56:11 +00:00
return {
2018-06-21 19:16:33 +00:00
content,
contentSyncData: {
2018-06-07 23:56:11 +00:00
...contentSyncData,
2018-06-21 19:16:33 +00:00
hash: content.hash,
2018-06-07 23:56:11 +00:00
},
};
2017-12-23 18:25:14 +00:00
},
2018-06-21 19:16:33 +00:00
async downloadWorkspaceData({ token, syncData }) {
2017-12-23 18:25:14 +00:00
if (!syncData) {
2018-06-07 23:56:11 +00:00
return {};
2017-12-23 18:25:14 +00:00
}
2018-06-07 23:56:11 +00:00
const content = await googleHelper.downloadFile(token, syncData.id);
2018-05-13 13:27:33 +00:00
const item = JSON.parse(content);
2018-06-07 23:56:11 +00:00
return {
item,
syncData: {
...syncData,
hash: item.hash,
},
};
2017-12-23 18:25:14 +00:00
},
2018-06-21 19:16:33 +00:00
async uploadWorkspaceContent({
token,
content,
file,
fileSyncData,
ifNotTooLate,
}) {
let gdriveFile;
let newFileSyncData;
2018-06-07 23:56:11 +00:00
2018-06-21 19:16:33 +00:00
if (fileSyncData) {
2018-06-07 23:56:11 +00:00
// Only update file media
2018-06-21 19:16:33 +00:00
gdriveFile = await googleHelper.uploadFile({
2018-06-07 23:56:11 +00:00
token,
media: Provider.serializeContent(content),
2018-06-21 19:16:33 +00:00
fileId: fileSyncData.id,
2018-06-07 23:56:11 +00:00
ifNotTooLate,
2018-05-13 13:27:33 +00:00
});
2018-06-07 23:56:11 +00:00
} else {
// Create file with media
2018-05-13 13:27:33 +00:00
const workspace = store.getters['workspace/currentWorkspace'];
2018-06-21 19:16:33 +00:00
const parentSyncData = store.getters['data/syncDataByItemId'][file.parentId];
gdriveFile = await googleHelper.uploadFile({
2018-06-07 23:56:11 +00:00
token,
2018-06-21 19:16:33 +00:00
name: file.name,
2018-06-07 23:56:11 +00:00
parents: [parentSyncData ? parentSyncData.id : workspace.folderId],
2018-05-13 13:27:33 +00:00
appProperties: {
2018-06-21 19:16:33 +00:00
id: file.id,
2018-05-13 13:27:33 +00:00
folderId: workspace.folderId,
},
2018-06-07 23:56:11 +00:00
media: Provider.serializeContent(content),
2018-05-13 13:27:33 +00:00
ifNotTooLate,
});
2018-06-07 23:56:11 +00:00
2018-06-21 19:16:33 +00:00
// Create file sync data
newFileSyncData = {
id: gdriveFile.id,
2018-07-17 19:58:40 +00:00
parentIds: gdriveFile.parents,
2018-06-21 19:16:33 +00:00
itemId: file.id,
type: file.type,
hash: file.hash,
};
2018-05-13 13:27:33 +00:00
}
2018-06-07 23:56:11 +00:00
// Return new sync data
return {
2018-06-21 19:16:33 +00:00
contentSyncData: {
id: `${gdriveFile.id}/content`,
itemId: content.id,
type: content.type,
hash: content.hash,
},
fileSyncData: newFileSyncData,
2018-06-07 23:56:11 +00:00
};
},
2018-06-21 19:16:33 +00:00
async uploadWorkspaceData({
token,
item,
syncData,
ifNotTooLate,
}) {
2018-06-07 23:56:11 +00:00
const workspace = store.getters['workspace/currentWorkspace'];
const file = await googleHelper.uploadFile({
token,
name: JSON.stringify({
id: item.id,
type: item.type,
hash: item.hash,
}),
parents: [workspace.dataFolderId],
appProperties: {
folderId: workspace.folderId,
},
media: JSON.stringify(item),
mediaType: 'application/json',
2018-06-07 23:56:11 +00:00
fileId: syncData && syncData.id,
oldParents: syncData && syncData.parentIds,
ifNotTooLate,
});
// Return new sync data
return {
2018-06-21 19:16:33 +00:00
syncData: {
id: file.id,
2018-07-17 19:58:40 +00:00
parentIds: file.parents,
2018-06-21 19:16:33 +00:00
itemId: item.id,
type: item.type,
hash: item.hash,
},
2018-06-07 23:56:11 +00:00
};
2017-12-23 18:25:14 +00:00
},
2018-09-20 09:00:07 +00:00
async listFileRevisions({ token, fileSyncDataId }) {
const revisions = await googleHelper.getFileRevisions(token, fileSyncDataId);
2018-05-13 13:27:33 +00:00
return revisions.map(revision => ({
id: revision.id,
2018-09-19 08:59:22 +00:00
sub: `${googleHelper.subPrefix}:${revision.lastModifyingUser.permissionId}`,
2018-05-13 13:27:33 +00:00
created: new Date(revision.modifiedTime).getTime(),
2018-07-17 19:58:40 +00:00
}));
},
async loadFileRevision() {
// Revision are already loaded
return false;
2017-12-23 18:25:14 +00:00
},
2018-07-17 19:58:40 +00:00
async getFileRevisionContent({
token,
contentId,
2018-09-20 09:00:07 +00:00
fileSyncDataId,
2018-07-17 19:58:40 +00:00
revisionId,
}) {
2018-09-20 09:00:07 +00:00
const content = await googleHelper.downloadFileRevision(token, fileSyncDataId, revisionId);
2018-07-17 19:58:40 +00:00
return Provider.parseContent(content, contentId);
2017-12-23 18:25:14 +00:00
},
2017-12-10 23:49:20 +00:00
});