gdrive-storage
v0.1.0
Published
A beginner-friendly file/media storage SDK powered by the end user's own Google Drive. Sister package to gdrive-db — free image/video storage with no backend to host and no bill.
Maintainers
Readme
gdrive-storage
A beginner-friendly file/media storage SDK powered by the end user's own Google Drive.
import { DriveStorage, BrowserAuthProvider } from "gdrive-storage";
const auth = new BrowserAuthProvider({
clientId: "YOUR_GOOGLE_OAUTH_CLIENT_ID",
});
const storage = await DriveStorage.connect({ bucket: "my-app-media", auth });
const photo = await storage.upload(fileInput.files[0]);
const allFiles = await storage.list();
const bytes = await storage.download(photo.id);
const objectUrl = await storage.getObjectUrl(photo.id); // drop straight into <img src>
await storage.delete(photo.id);gdrive-storage is the sister package to gdrive-db —
install both and sign in once with the same Google account to get "a free database" (gdrive-db)
and "free image/video storage" (gdrive-storage) for a hobby project or prototype, with no
backend to host and no bill.
What it is
gdrive-storage gives your app a small file storage API — upload, list, get, download,
delete, exists, and a browser convenience getObjectUrl — while the actual files live inside a
MediaVault folder in the signed-in user's own Google Drive. There is no shared backend, no
storage bucket you have to host, and no bill: each user's files live in their own Drive, under
their own quota.
Why it exists
Beginners building a first app, a class project, or a weekend prototype often need "somewhere to
put uploaded images/videos" without standing up S3, Cloudinary, or Firebase Storage and wiring up
hosting, billing, and access rules. This package trades scale and guarantees for near-zero setup:
sign in with Google, and you have a working per-user file store. Pair it with
gdrive-db for a "free mini-backend" — JSON data plus
file storage, both riding on the same Google sign-in.
Who should use it
- Beginner developers and students learning to build apps that handle file/image/video uploads
- Small frontend projects, demos, and hackathon prototypes
- Hobby projects where "my own Google Drive" is an acceptable place to keep files
Who should NOT use it
- Anyone needing a shared/multi-user backend. Every bucket lives in one Google account's Drive; there is no built-in way for multiple users to read/write the same files.
- Anyone needing public/CDN-backed file hosting. There are no shareable links and no CDN — see Limitations.
- Anyone needing image/video processing. No resizing, thumbnailing, or transcoding — store and retrieve the original bytes only.
- Anyone who needs this to just work without the user signing in to Google. There is no "headless"/anonymous mode for end users — see Authentication.
This is not a free S3, Cloudinary, or Firebase Storage replacement. Think of it as: use your own Google Drive as simple file storage for small frontend projects and prototypes.
Architecture
Developer Application
|
v
gdrive-storage
|
v
Google OAuth (Identity Services, browser) / refresh-token OAuth (Node, server)
|
v
Google Drive REST API (multipart upload for small files, resumable upload for large files)
|
v
User's own Google Drive
|
v
MediaVault/<bucket>/
├── photo-abc123.jpg
├── clip-def456.mp4
└── ...Internally the package is layered so each piece can be tested and reasoned about independently:
- auth — obtains a live Drive access token (
TokenProviderinterface). Two interchangeable implementations:BrowserAuthProvider(browser) andNodeAuthProvider(server, separate entry). - drive —
DriveClient, a thinfetch-based wrapper over the Drive REST v3 API: folder find/create, multipart upload (small files), resumable upload (large files), download, list, delete, and metadata lookup. - storage —
FileStore, the bucket-folder abstraction on top ofDriveClient: upload routing (small vs. large), MIME-type and file-size validation. - top-level API —
DriveStorage(connect,upload,list,get,download,exists,delete,getObjectUrl).
Installation
npm install gdrive-storageFor server-side/Node usage of NodeAuthProvider, also install the optional peer dependency:
npm install googleapisAuthentication
This is the part beginners get wrong most often when reaching for Drive as a backend, so it's worth being explicit: there is no way to talk to a user's private Google Drive without that user signing in with Google. This package never asks for more than that.
Browser (the primary, recommended path)
BrowserAuthProvider uses Google Identity Services
(GIS) — the current Google-recommended way to get an OAuth access token directly in a browser. It
needs only a public OAuth Client ID, never a client secret:
import { BrowserAuthProvider, DriveStorage } from "gdrive-storage";
const auth = new BrowserAuthProvider({
clientId: "xxxx.apps.googleusercontent.com",
});
const storage = await DriveStorage.connect({ bucket: "my-app-media", auth });- The first Drive operation triggers Google's sign-in/consent popup.
- Access tokens are cached in memory only for the lifetime of the page — never written to
localStorage,sessionStorage,IndexedDB, or cookies. Reloading the page requires signing in again. Callauth.signOut()to discard the cached token explicitly. - No server component is required for this flow.
Server / Node (optional, advanced)
For a backend service that needs to act on Drive without a user present in a browser,
NodeAuthProvider (from the separate gdrive-storage/node entry) uses a long-lived Google
OAuth refresh token:
import { NodeAuthProvider } from "gdrive-storage/node";
import { DriveStorage } from "gdrive-storage";
const auth = new NodeAuthProvider({
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID!,
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!,
refreshToken: process.env.GOOGLE_OAUTH_REFRESH_TOKEN!,
});
const storage = await DriveStorage.connect({ bucket: "my-app-media", auth });This entry point is kept completely separate from the main package export (a different file,
marked external in the build) so bundlers building browser code never see a reference to
googleapis, and a client secret can never accidentally end up in a browser bundle. You obtain the
refresh token yourself, once, through your own server-side OAuth consent flow — this package does
not provide that flow, since it's inherently specific to your backend.
Never put clientSecret or refreshToken in browser code, in a NEXT_PUBLIC_*/VITE_*
variable, or anywhere a client can read it.
Google Cloud setup
- Go to the Google Cloud Console and create (or select) a project.
- Enable the Google Drive API for that project (APIs & Services -> Library -> "Google Drive API" -> Enable).
- Configure the OAuth consent screen (APIs & Services -> OAuth consent screen). For personal projects, "External" + test users is usually the right choice while developing.
- Create credentials (APIs & Services -> Credentials -> Create Credentials -> OAuth client ID):
- For browser usage: Web application, with your app's origin(s) under "Authorized JavaScript
origins". This gives you the public Client ID used by
BrowserAuthProvider. - For server usage only: create a client ID/secret and obtain a refresh token yourself via a one-time OAuth consent flow you run on your server.
- For browser usage: Web application, with your app's origin(s) under "Authorized JavaScript
origins". This gives you the public Client ID used by
- Copy
.env.exampleto.envand fill in your own values. Never commit.env.
Required scope
The package requests only:
https://www.googleapis.com/auth/drive.fileThis is Google's least-privilege Drive scope: it grants access only to files/folders the app
itself creates, not the user's entire Drive. gdrive-storage never asks for broader access
than this.
Environment configuration
See .env.example. GOOGLE_OAUTH_CLIENT_ID is public and safe to ship in
browser code. GOOGLE_OAUTH_CLIENT_SECRET and GOOGLE_OAUTH_REFRESH_TOKEN are server-only secrets
— only needed for NodeAuthProvider, and must never reach browser code or version control.
Frontend usage (Browser)
This is the primary, recommended way to use gdrive-storage — no server component required. Each
end user signs in with their own Google account and files are stored in their own Drive.
1. Install and get a Client ID
npm install gdrive-storageFollow Google Cloud setup below once to get a public OAuth Client ID (looks
like xxxx.apps.googleusercontent.com).
2. Connect
import { DriveStorage, BrowserAuthProvider } from "gdrive-storage";
const auth = new BrowserAuthProvider({
clientId: "YOUR_GOOGLE_OAUTH_CLIENT_ID",
});
const storage = await DriveStorage.connect({ bucket: "my-app-media", auth });Calling connect() (or any other method before it) triggers Google's sign-in/consent popup the
first time. Behind the scenes it finds-or-creates a MediaVault/my-app-media/ folder in the
signed-in user's Drive and scopes every subsequent call to that folder.
3. Upload a file picked with <input type="file">
<input id="fileInput" type="file" accept="image/*,video/*" />const fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", async () => {
const file = fileInput.files[0];
if (!file) return;
const uploaded = await storage.upload(file, {
onProgress: (pct) => console.log(`Upload progress: ${pct}%`),
});
console.log(uploaded);
// => { id, name, mimeType, size, createdTime, modifiedTime }
});upload() reads the File's name and MIME type automatically — you don't need to pass them
yourself unless you want to override them.
4. Show it with <img> / <video>
const objectUrl = await storage.getObjectUrl(uploaded.id);
document.getElementById("preview").src = objectUrl;
// When the element is removed / you no longer need the preview:
URL.revokeObjectURL(objectUrl);5. List, check, and delete
const allFiles = await storage.list(); // metadata for every file in the bucket
const meta = await storage.get(uploaded.id); // metadata only, or null if missing
const isThere = await storage.exists(uploaded.id);
await storage.delete(uploaded.id);6. Sign out
auth.signOut(); // discards the in-memory token; next call re-triggers sign-inSee examples/browser/index.html for a complete working page
(upload input, live preview, file list with delete buttons).
Backend usage (Node / server)
Use this when a server process needs to read/write a Drive on its own behalf — for example a background job, an admin script, or a backend that manages one dedicated Google account's files. This is not the path for "each of my app's end users has their own Drive" — that's the frontend flow above.
1. Install
npm install gdrive-storage googleapisgoogleapis is an optional peer dependency, only required for NodeAuthProvider.
2. Get a refresh token (one-time, done by you)
NodeAuthProvider needs a Client ID, Client Secret, and a long-lived refresh token for the Google
account whose Drive you want to use. Obtain these once, yourself, through your own server-side
OAuth consent flow — this package does not provide that flow, since the details depend on your
backend/framework. In short:
- Create an OAuth Client ID of type Desktop app or Web application in the Google Cloud Console (see Google Cloud setup).
- Run a one-time OAuth consent flow (e.g. with the
googleapispackage'sgoogle.auth.OAuth2client andaccess_type: "offline") to get a refresh token for the target account. - Store the Client ID, Client Secret, and refresh token as server-only environment variables — never commit them, never expose them to browser code.
3. Connect and use
import { NodeAuthProvider } from "gdrive-storage/node";
import { DriveStorage } from "gdrive-storage";
import { readFile } from "node:fs/promises";
const auth = new NodeAuthProvider({
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID!,
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!,
refreshToken: process.env.GOOGLE_OAUTH_REFRESH_TOKEN!,
});
const storage = await DriveStorage.connect({ bucket: "my-app-media", auth });
// Upload a Buffer read from disk, or any Uint8Array/ArrayBuffer you already have
const fileBuffer = await readFile("./clip.mp4");
const uploaded = await storage.upload(fileBuffer, {
name: "clip.mp4",
mimeType: "video/mp4",
});
// List / get / download / delete work exactly the same as in the browser
const allFiles = await storage.list();
const meta = await storage.get(uploaded.id);
const bytes = await storage.download(uploaded.id); // Uint8Array
await storage.delete(uploaded.id);getObjectUrl() is browser-only (it relies on URL.createObjectURL) and throws
DriveStorageError if called in Node — use download() and write the bytes to disk, a response
stream, or wherever you need them instead:
import { writeFile } from "node:fs/promises";
const bytes = await storage.download(uploaded.id);
await writeFile("./downloaded-clip.mp4", bytes);Never put clientSecret or refreshToken in browser code, in a NEXT_PUBLIC_*/VITE_*
variable, or anywhere a client can read it. This entry point (gdrive-storage/node) is kept
completely separate from the main package export precisely so bundlers building browser code never
even see a reference to googleapis.
API reference
Applies the same way whether auth came from BrowserAuthProvider or NodeAuthProvider — the
DriveStorage instance itself doesn't know or care which environment it's running in.
Bucket options
const storage = await DriveStorage.connect({
bucket: "my-app-media",
auth,
acceptedTypes: ["image/*", "video/*"], // optional allow-list; open (any type) by default
maxFileSize: 50 * 1024 * 1024, // optional, in bytes; defaults to 100MB
});Methods
| Method | Description |
| ------------------------ | ------------------------------------------------------------------- |
| upload(data, options?) | Upload a File/Blob/Buffer/Uint8Array/ArrayBuffer, returns metadata. |
| list() | Return metadata for every file in the bucket. |
| get(id) | Return one file's metadata, or null if not found. |
| download(id) | Return the file's raw bytes as a Uint8Array. Throws if not found. |
| exists(id) | Whether a file with this id exists. |
| delete(id) | Delete a file by id. Throws if not found. |
| getObjectUrl(id) | Browser only: downloads and wraps the file as an object URL. |
upload() picks Drive's multipart upload for files at or under 5MB, and resumable upload above
that — you don't need to choose.
Limitations
- No public/shareable links.
drive.file-scoped files are private to the app+user; there is no "anyone with the link can view" mode without a broader, more privacy-invasive OAuth scope this package deliberately does not request. - No image/video transformations. No resizing, thumbnailing, cropping, or transcoding — store and retrieve the original bytes only.
- No CDN. Every read (
download,getObjectUrl) is an authenticated round trip to Drive. - No dedup, no versioning, no subfolders. Flat namespace per bucket — uploading twice creates two separate files, and there is no folder nesting beyond the one bucket folder.
- Upload progress is coarse in this MVP —
onProgressfires with0then100; granular chunked-upload progress is a fast-follow (see Future roadmap).
Security
- Files live inside the signed-in user's own Google Drive, under
MediaVault/<bucket>/. They are never made public and never shared via "anyone with the link" — every request is authenticated per-user through OAuth. - The package requests the least-privilege
drive.filescope: only files/folders it creates itself, not the user's whole Drive. - This package never sees or handles the user's Google password — only an OAuth access token, obtained through Google's own sign-in UI.
- Browser access tokens are kept in memory only; nothing is written to
localStorageor similar. - A Node client secret / refresh token, if you use
NodeAuthProvider, is your responsibility to store securely (environment variables, a secrets manager) — this package never logs or persists them. - This package cannot make claims about the security of your Google account itself. You remain responsible for your own Google account security (strong password, 2FA, reviewing connected apps). Nothing here is "100% secure" or "completely private" — it inherits whatever security posture your Google account and network have.
Storage & quota considerations
Even though Google Drive accounts often have generous storage quotas, this package is still constrained by:
- Your Google account's Drive storage quota (uploaded files count against it like any other file)
- Google Drive API rate limits and quotas
- Network speed (every upload/download is a round trip to Drive)
- Browser/Node memory (a whole file's bytes are held in memory for upload/download — there is no streaming in this MVP)
Development
npm install
npm run dev # tsup --watch
npm run build # ESM + CJS + .d.ts output in dist/
npm run typecheckTesting
npm test # vitest run
npm run test:watchTests never make real Google API calls. DriveClient is tested against an injected fetch mock;
higher layers (FileStore, DriveStorage) are tested against a shared in-memory FakeDriveClient
test double, so the whole connect → upload/list/download/delete flow is exercised without any
network access.
Build
npm run buildProduces dist/index.{js,mjs,d.ts} (main, browser-safe entry) and dist/node.{js,mjs,d.ts}
(server-only entry, gdrive-storage/node).
Publishing
Not published yet. Before publishing, verify the package contents:
npm pack --dry-runConfirm the tarball contains only dist/, README.md, and LICENSE — no test files, no .env,
no remaining-tasks.md, no source .ts files.
Future roadmap
Not implemented yet, listed here as ideas for later versions:
- Granular, chunked resumable-upload progress
- Public/shareable link support behind an explicit, separately-documented opt-in
- File dedup and versioning
- Client-side image/video thumbnailing before upload
- Streaming upload/download for very large files (avoid holding the whole file in memory)
- Migration tools, admin UI
Author
Built by Syed Muhammad Ali.
License
MIT — see LICENSE.
