Skip to content

Commit

Permalink
Updates rewritten drive client
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmerfield committed Feb 13, 2025
1 parent b753be4 commit 74686be
Show file tree
Hide file tree
Showing 21 changed files with 413 additions and 823 deletions.
25 changes: 0 additions & 25 deletions app/clients/google-drive-2/README

This file was deleted.

12 changes: 0 additions & 12 deletions app/clients/google-drive-2/index.js

This file was deleted.

64 changes: 0 additions & 64 deletions app/clients/google-drive-2/test.js

This file was deleted.

66 changes: 17 additions & 49 deletions app/clients/google-drive/README
Original file line number Diff line number Diff line change
@@ -1,57 +1,25 @@
Fix, I have confirmed that the uploader of the file, regardless of the owner of the folder into which it is uploaded, pays the storage 'cost' for the file. To demonstate this I used two google drive accounts ([email protected] and [email protected])
# Google Drive Client for Blot

I created a shared folder in [email protected], then shared it with [email protected]. I uploaded a file into it using [email protected]'s account. The storage usage for [email protected] went up, but [email protected]'s storage usage did not.
This repository contains the code for Blot’s Google Drive Client, designed to handle all interactions with Google Drive for the Blot blogging platform. The client is responsible for managing shared folders, syncing changes, and facilitating smooth integration between Blot and Google Drive.

---
There are two 'apps', one external which requests the user's email address through the OAUTH flow.
The other is a 'service account' which has access to a Drive folder created by [email protected]

The service account creates a new site folder inside the Sites folder and then shares it with the user's email. I created

Add new redirect URIs here:
https://console.cloud.google.com/apis/credentials?project=quickstart-1585441405190
## How It Works
1. User Authentication:
Users log in via Blot’s external app, which captures their Google email address.
2. Folder Creation and Sharing:
The internal Google Drive Client uses the captured email to:
• Create a folder in Blot’s Google Drive account.
• Share the folder with the user (role=editor).
3. Change Tracking and Syncing:
• Monitor the shared folder for changes using the Google Drive API.
• Sync updates, including file uploads, edits, and deletions, to Blot’s server.

---

Questions
- is there an overhead to creating the oauth2 client each time? can we safely reuse it?
- is it possible to restrict Blot's access to a single folder in the google drive?
- it doesn't seem like it is

I believe we will need this API for drive changes:
https://developers.google.com/drive/activity/v2

Since the changes.list api doesn't return rename events, it seems.

The webhooks are now tunnelled through webhooks.blot.im using clients/webhooks.js

Test server
```
http://localhost:8822/clients/googledrive/authenticate
```

Resources:

[Verify domain ownership](https://search.google.com/search-console/settings?resource_id=sc-domain%3Ablot.im)

[Domain verification for API use](https://console.cloud.google.com/apis/credentials/domainverification?organizationId=683828060430&project=quickstart-1585441405190)

[Console for redirect URIs](https://console.cloud.google.com/apis/credentials/oauthclient/32772360147-pnntpgr8pjnlem4m6s1perkju3ghce3b.apps.googleusercontent.com?project=quickstart-1585441405190&pli=1)

[Console for managing API permissions](https://console.developers.google.com/apis/credentials/oauthclient/32772360147-pnntpgr8pjnlem4m6s1perkju3ghce3b.apps.googleusercontent.com?project=quickstart-1585441405190)

[Downloading files google drive](https://stackoverflow.com/questions/62476413/google-drive-api-downloading-file-nodejs)

[Downloading files google drive](https://developers.google.com/drive/api/v3/manage-downloads)

[Link to remove connection during testing](https://myaccount.google.com/permissions?pli=1)

Documentation:

[nodejs-client authentication-and-authorization](https://github.com/googleapis/google-api-nodejs-client#authentication-and-authorization)

[quickstart](https://developers.google.com/drive/api/v3/quickstart/nodejs)
SHould we ask the user to share a folder with our email? This was it's a 'true' folder and doesn't show up in 'shared with me'.

Actions to test:
- Mass rename -> mass move
- Mass move -> mass rename
- Delete -> Restore -> Delete
- Move file from outside folder into folder
- Move file from inside folder out of folder
How is storage usage affected by 'service accounts'?
49 changes: 3 additions & 46 deletions app/clients/google-drive/database.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const client = require("models/client");
const promisify = require("util").promisify;

const client = require("models/client");
const set = promisify(client.set).bind(client);
const get = promisify(client.get).bind(client);
const del = promisify(client.del).bind(client);
Expand All @@ -8,20 +9,11 @@ const hget = promisify(client.hget).bind(client);
const hdel = promisify(client.hdel).bind(client);
const hscan = promisify(client.hscan).bind(client);

const INVALID_ACCOUNT_STRING =
"Google Drive client: Error decoding JSON for account of blog ";

const database = {
keys: {
// Used to renew webhooks for all connected Google Drives
allAccounts: "clients:google-drive:all-accounts",

// Used to determine if another blog is connected to a
// given Google Drive account during the disconnection process
allBlogs: function (permissionId) {
return "clients:google-drive:" + permissionId + ":blogs";
},

account: function (blogID) {
return "blog:" + blogID + ":google-drive:account";
},
Expand Down Expand Up @@ -65,32 +57,13 @@ const database = {
try {
account = JSON.parse(account);
} catch (e) {
return callback(new Error(INVALID_ACCOUNT_STRING + blogID));
return callback(e);
}

callback(null, account);
});
},

// During account disconnection from Google Drive
// on Blot's folder settings page we need to determine
// whether or not to revoke the credentials, which
// unfortunately has global effects and would tank
// other blogs also connected to this Google Drive account.
canRevoke: function (permissionId, callback) {
let canRevokeCredentials;

if (!permissionId) {
canRevokeCredentials = true;
return callback(null, canRevokeCredentials);
}

client.smembers(this.keys.allBlogs(permissionId), (err, blogs) => {
canRevokeCredentials = !blogs || blogs.length < 2;
callback(null, canRevokeCredentials);
});
},

setAccount: function (blogID, changes, callback) {
const key = this.keys.account(blogID);

Expand All @@ -99,18 +72,6 @@ const database = {

const multi = client.multi();

if (changes.permissionId)
multi.sadd(this.keys.allBlogs(changes.permissionId), blogID);

// Clean up if the permissionId for a blog changes
if (
changes.permissionId &&
account.permissionId &&
changes.permissionId !== account.permissionId
) {
multi.srem(this.keys.allBlogs(account.permissionId), blogID);
}

for (var i in changes) {
account[i] = changes[i];
}
Expand All @@ -129,10 +90,6 @@ const database = {

multi.del(this.keys.account(blogID)).srem(this.keys.allAccounts, blogID);

if (account && account.permissionId) {
multi.srem(this.keys.allBlogs(account.permissionId), blogID);
}

if (account && account.folderId) {
multi
.del(this.folder(account.folderId).key)
Expand Down
104 changes: 35 additions & 69 deletions app/clients/google-drive/disconnect.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,39 @@
const config = require("config");
const database = require("./database");
const google = require("googleapis").google;
const Blog = require("models/blog");
const establishSyncLock = require("./util/establishSyncLock");
const database = require("./database");
const createDriveClient = require("./util/createDriveClient");
const debug = require("debug")("blot:clients:google-drive");

async function disconnect(blogID, callback) {
let done;

try {
let lock = await establishSyncLock(blogID);
done = lock.done;
} catch (err) {
return callback(err);
}

debug("getting account info");
const account = await database.getAccount(blogID);

if (account && account.access_token && account.refresh_token) {
const canRevoke = await database.canRevoke(account.permissionId);
const auth = new google.auth.OAuth2(
config.google.drive.key,
config.google.drive.secret
);

auth.setCredentials({
refresh_token: account.refresh_token,
access_token: account.access_token,
});

if (account.channel) {
try {
debug("attempting to stop listening to webhooks");
const drive = google.drive({ version: "v3", auth });
await drive.channels.stop({
requestBody: account.channel,
});
debug("stop listening to webhooks successfully");
} catch (e) {
debug("failed to stop webhooks but no big deal:", e.message);
debug("it will expire automatically");
}
}

// We need to preserve Blot's access to this Google
// Drive account if another blog uses it. Unfortunately
// it seems impossible to simple revoke one blog's access
// other blogs connected to the account lose access too.
if (canRevoke) {
try {
debug("Trying to revoke Google API credentials");
// destroys the oauth2Client's active
// refresh_token and access_token
await auth.revokeCredentials();
} catch (e) {
debug("Failed to revoke but token should expire naturally", e.message);
}
}
}

debug("dropping blog from database");
await database.dropAccount(blogID);

debug("resetting client setting");
Blog.set(blogID, { client: "" }, async function (err) {
await done(err);
callback();
});
}

module.exports = disconnect;
module.exports = async (blogID, callback) => {
let done;

try {
let lock = await establishSyncLock(blogID);
done = lock.done;
} catch (err) {
return callback(err);
}

const account = await database.getAccount(blogID);

if (account && account.channel) {
const drive = await createDriveClient(blogID);
try {
debug("attempting to stop listening to webhooks");
await drive.channels.stop({
requestBody: account.channel,
});
debug("stop listening to webhooks successfully");
} catch (e) {
debug("failed to stop webhooks but no big deal:", e.message);
debug("it will expire automatically");
}
}

await database.dropAccount(blogID);

Blog.set(blogID, { client: "" }, async function (err) {
await done(err);
callback();
});
};
3 changes: 2 additions & 1 deletion app/clients/google-drive/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const database = require("./database");
module.exports = async function remove(blogID, path, callback) {
const prefix = () => clfdate() + " Google Drive:";
try {
const { drive, account } = await createDriveClient(blogID);
const drive = await createDriveClient(blogID);
const account = await database.getAccount(blogID);
const { getByPath } = database.folder(account.folderId);

console.log(prefix(), "Looking up fileId for", path);
Expand Down
Loading

0 comments on commit 74686be

Please sign in to comment.