@googledrive/index
v2.5.8
Published
Google Drive Index - A serverless Google Drive directory listing on Cloudflare Workers
Maintainers
Readme
Google Drive Index
A fast, modern, serverless Google Drive directory listing powered by Cloudflare Workers. Browse, search, stream, and share your Google Drive files through a beautiful web interface — no server required.
Table of Contents
- Features
- Architecture Overview
- Prerequisites
- Quick Start
- Configuration Reference
- Multiple Drives
- Per-Drive Credentials
- Cross-Drive ID Lookup
- Service Account Setup
- Login & Authentication
- Per-Folder Password Protection
- Encryption Keys
- Download URL Protection
- Region & ASN Blocking
- Load Balancing (Multiple Download Domains)
- Themes
- Media Players
- File Viewers
- Search
- Deployment
- Development Setup
- Build Process (CDN Assets)
- API Reference
- Troubleshooting / FAQ
- Planned Features
- Changelog
- Credits
- Sponsors
- License
Features
Core
- Personal Drive (My Drive) and Shared/Team Drive support
- Multiple drives on a single deployment with a unified homepage
- Auto-discover Shared Drives in the generator — fetch all drives from your account and add them with one click
- Serverless — runs entirely on Cloudflare Workers (free tier supported)
- No database required for basic operation
- Infinite scroll with paginated file listing
- Full-text search across one or all configured drives
- File ID → path resolution for bookmarking/sharing files
- Google Workspace export — Google Docs, Sheets, and Slides appear in the listing and can be exported to PDF, DOCX/XLSX/PPTX, TXT/CSV
- Storage quota display — optional bar showing Drive usage (disabled by default, enable via
show_quota)
Security
- Username/password login with encrypted session cookies (AES-CBC + HMAC-SHA256)
- Google OAuth sign-in for whitelisted Google accounts
- Cloudflare KV user database for dynamic user management
- Per-folder
.passwordfile protection - Encrypted, expiring download links (no direct Google Drive exposure)
- IP-locked download links (optional)
- Single-session enforcement (optional)
- IP-change logout (optional)
- Region/ASN blocking
- Referer-based direct-link protection
UI/UX
- Dark and Light themes with auto-detect from system preference
- Responsive design — mobile, tablet, desktop
- Bootstrap 5 with 26+ Bootswatch themes available
- Breadcrumb navigation
- Column sorting by name, size, modified date
- Folder filter (real-time search within current directory)
- Bulk file selection and copy links
- README.md rendering below file list
- HEAD.md rendering above file list
Media
- Video player — Plyr, Video.js, DPlayer, or JWPlayer
- Audio player — APlayer with auto-playlist for audio folders
- PDF viewer — PDF.js with page navigation and zoom
- Image viewer with lazy loading
- Code viewer with syntax-aware display (up to 2 MB)
- HLS / m3u8 stream support
Architecture Overview
Browser
│
▼
Cloudflare Workers (src/worker.js)
│ ├─ Serves HTML shell (Bootstrap + app.js)
│ ├─ POST /{n}:/ ─────────► Google Drive API v3 (files.list)
│ ├─ POST /{n}:search ────► Google Drive API v3 (files.list with q=)
│ ├─ GET /download.aspx ─► Google Drive API v3 (files.get?alt=media or files.export for Workspace)
│ ├─ GET /{n}:quota ─► Google Drive API v3 (about.get?fields=storageQuota)
│ ├─ GET /findpath?id= ─► Cross-drive path lookup + redirect
│ ├─ GET /?driveid= ─► Cross-drive raw ID lookup + redirect
│ ├─ POST /copy ─► Google Drive API v3 (files/copy)
│ └─ Auth: AES-CBC session cookie, HMAC-signed download links
│
▼
app.js (frontend — loaded from jsDelivr CDN)
│ ├─ Renders file list, breadcrumbs, search
│ └─ Loads media players on demand
│
▼
Google Drive APIKey design decisions:
- Worker caches drive initialisation in memory (warm starts are fast)
- File IDs are encrypted before being sent to the browser; the browser never sees real Google Drive IDs
- Download links are HMAC-signed + time-limited (default 7 days)
- Assets (CSS/JS) are served from jsDelivr via the npm package so Worker CPU time is not wasted on static files
Prerequisites
| Requirement | Notes |
|---|---|
| Cloudflare account | Free tier works. Workers free plan: 100k requests/day |
| Google Cloud project | Free. Needed to enable the Drive API |
| OAuth 2.0 credentials or a Service Account JSON key | See below |
| Node.js 18+ | Only for local development / CLI deployment |
| wrangler CLI | npm i -g wrangler — only for CLI deployment |
Quick Start
Method A: OAuth 2.0 (Personal Drive)
Step 1 — Google Cloud Setup
- Go to Google Cloud Console → create or select a project
- APIs & Services → Enable APIs → enable Google Drive API
- APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID
- Application type: Desktop app
- Copy the
client_idandclient_secret
- Generate a
refresh_tokenby visiting the generator at bdi-generator.hashhackers.com or running the OAuth flow manually (see Troubleshooting)
Step 2 — Configure src/worker.js
Open src/worker.js and fill in the top section:
const authConfig = {
"siteName": "My Drive Index",
"client_id": "123456789-abc.apps.googleusercontent.com",
"client_secret": "GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx",
"refresh_token": "1//xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"roots": [
{ "id": "root", "name": "My Drive", "protect_file_link": false }
]
};Also change the encryption keys (see Encryption Keys):
const crypto_base_key = "YOUR_32_CHAR_HEX_KEY";
const hmac_base_key = "YOUR_64_CHAR_HEX_KEY";Step 3 — Deploy
Option A — Dashboard (no CLI):
- Go to dash.cloudflare.com → Workers & Pages → Create Application → Create Worker
- Click Edit Code, paste the full contents of
src/worker.js, click Save and Deploy
Option B — CLI:
npm install
npx wrangler deployMethod B: Service Account (Shared Drive)
Service accounts are ideal for Shared/Team Drives where you don't want to share an OAuth refresh token.
- Google Cloud Console → IAM & Admin → Service Accounts → Create Service Account
- Download the JSON key file
- In Google Drive, share the drive/folder with the service account email (
[email protected]) as a Viewer - Paste the JSON content into
src/worker.js:
const serviceaccounts = [
{
"type": "service_account",
"project_id": "your-project",
"private_key_id": "abc123",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "...",
"token_uri": "https://oauth2.googleapis.com/token"
}
];- Set
"service_account": trueinauthConfig - Set the shared drive ID in
roots:
"roots": [
{ "id": "0AOM2i7Mi3uWIUk9PVA", "name": "Team Drive", "protect_file_link": false }
]Multiple service accounts — add multiple objects to
serviceaccounts[]. One is picked at random per request to distribute API quota.
Configuration Reference
authConfig
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| siteName | string | "Google Drive Index" | Page title in the browser tab and navbar |
| client_id | string | "" | Google OAuth 2.0 client ID |
| client_secret | string | "" | Google OAuth 2.0 client secret |
| refresh_token | string | "" | OAuth refresh token for accessing the drive |
| service_account | bool | false | Set true to use Service Account auth instead of OAuth |
| service_account_json | object | auto | Don't touch — points to the randomly selected service account |
| files_list_page_size | number | 100 | Files fetched per API page (max 1000). Higher = fewer API calls but slower first load |
| search_result_list_page_size | number | 100 | Search results per API page |
| enable_cors_file_down | bool | false | Add Access-Control-Allow-Origin: * to file downloads |
| enable_password_file_verify | bool | false | Enable per-folder .password file protection |
| direct_link_protection | bool | false | Block direct download links used without a Referer from your domain |
| disable_anonymous_download | bool | false | Require login session for all file downloads |
| file_link_expiry | number | 7 | Days until a generated download link expires |
| search_all_drives | bool | true | When true, search spans all user drives; false restricts to current drive |
| enable_login | bool | false | Enable the username/password login system |
| enable_signup | bool | false | Reserved for future signup support |
| enable_social_login | bool | false | Show Google sign-in button on login page |
| google_client_id_for_login | string | "" | OAuth client ID for Google login (different from drive access) |
| google_client_secret_for_login | string | "" | OAuth client secret for Google login |
| redirect_domain | string | "" | Your worker URL, e.g. https://index.example.com — required for Google OAuth login |
| login_database | string | "Local" | "Local" (in-config users) or "KV" (Cloudflare KV store) |
| login_days | number | 7 | Session duration in days |
| enable_ip_lock | bool | false | Bind download links to the requesting user's IP address |
| single_session | bool | false | Only allow one active session per user (uses KV) |
| ip_changed_action | bool | false | Log out the user if their IP address changes (uses KV) |
| cors_domain | string | "*" | Access-Control-Allow-Origin for API responses |
| users_list | array | [{username, password}] | Local user accounts (only used when login_database: "Local") |
| roots | array | — | Required. List of drives/folders to index (see Multiple Drives) |
roots array
Each entry in roots describes one drive or folder:
| Key | Type | Description |
|-----|------|-------------|
| id | string | "root" for My Drive; a Shared Drive ID (from the URL); or a specific folder ID |
| name | string | Display name shown in the navbar dropdown and homepage grid |
| protect_file_link | bool | If true and login is enabled, requires authentication even for direct file downloads |
| client_id | string | (optional) Per-drive OAuth client ID — overrides global client_id |
| client_secret | string | (optional) Per-drive OAuth client secret — overrides global client_secret |
| refresh_token | string | (optional) Per-drive refresh token — overrides global refresh_token |
| service_account | bool | (optional) Set true to use per-drive service account auth |
| service_account_json | object | (optional) Service account JSON for this drive (instead of global serviceaccounts[]) |
If none of the per-drive credential keys are present, the drive falls back to the global client_id/client_secret/refresh_token or serviceaccounts[]. See Per-Drive Credentials for a full example.
uiConfig
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| theme | string | "darkly" | Bootstrap/Bootswatch theme name. Options: darkly, flatly, slate, cyborg, journal, lumen, materia, minty, pulse, sandstone, simplex, sketchy, solar, spacelab, superhero, united, yeti, quartz, morph, vapor, zephyr |
| version | string | CDN_VERSION | Set automatically by build script — used for npm CDN asset versioning |
| debug_mode | bool | false | Show a debug panel in the page footer with live API request logs, error traces, and page info. Useful during development; disable for production. |
| logo_image | bool | true | true = use an image URL for the logo; false = use plain text |
| logo_height | string | "" | CSS height of the logo image (e.g. "30px") |
| logo_width | string | "100px" | CSS width of the logo image |
| favicon | string | CDN URL | URL to the favicon |
| logo_link_name | string | CDN SVG URL | If logo_image: true, the image URL; if false, the text to show |
| fixed_header | bool | true | Keep the navbar fixed at the top while scrolling |
| header_padding | string | "80" | Top padding for page content (pixels). Use 80 with fixed header, 20 otherwise |
| nav_link_1 | string | "Home" | Unused navigation label (reserved) |
| nav_link_3 | string | "Current Path" | Unused navigation label (reserved) |
| nav_link_4 | string | "Contact" | Unused navigation label (reserved) |
| fixed_footer | bool | false | Fix footer to the bottom of the viewport |
| hide_footer | bool | true | Completely hide the footer |
| header_style_class | string | "navbar-dark bg-primary" | Bootstrap classes for the navbar background |
| footer_style_class | string | "bg-primary" | Bootstrap classes for the footer background |
| css_a_tag_color | string | "white" | Link colour in the navbar |
| css_p_tag_color | string | "white" | Paragraph colour in the navbar |
| folder_text_color | string | "white" | Folder name colour in listings |
| loading_spinner_class | string | "text-light" | Bootstrap colour class for the loading spinner |
| search_button_class | string | "btn btn-danger" | Bootstrap classes for the search submit button |
| path_nav_alert_class | string | "alert alert-primary" | Bootstrap classes for the path alert box |
| file_view_alert_class | string | "alert alert-danger" | Bootstrap classes for file view alerts |
| file_count_alert_class | string | "alert alert-secondary" | Bootstrap classes for the item count bar |
| contact_link | string | Telegram URL | URL for the contact button in the navbar |
| copyright_year | number | auto | Current year — auto-detected, do not change |
| company_name | string | "The Bay Index" | Name shown in the footer copyright |
| company_link | string | Telegram URL | URL for the footer company name |
| credit | bool | true | Show "Redesigned by..." credit in footer |
| display_size | bool | true | Show file sizes in the listing |
| display_time | bool | false | Show file modification timestamps in the listing |
| display_download | bool | true | Show download icon next to each file in the listing |
| disable_player | bool | false | Disable all in-browser media players; files open/download directly |
| disable_video_download | bool | false | Hide download and copy buttons in the video player view |
| allow_selecting_files | bool | true | Enable checkboxes for bulk file selection / link copying |
| second_domain_for_dl | bool | false | Route all downloads through a secondary Worker domain (see Load Balancing) |
| poster | string | CDN URL | Default video poster/thumbnail image URL |
| audioposter | string | CDN URL | Default audio cover art image URL |
| disable_audio_download | bool | false | Hide the download button in the audio player |
| jsdelivr_cdn_src | string | jsDelivr URL | Base CDN URL for assets — change only if self-hosting |
| render_head_md | bool | true | Render HEAD.md as HTML above the file listing |
| render_readme_md | bool | true | Render README.md as HTML below the file listing |
| unauthorized_owner_link | string | Telegram URL | Link shown on unauthorised error pages |
| unauthorized_owner_email | string | abuse email | Email shown on unauthorised error pages |
| downloaddomain | string | auto | Set by domain_for_dl — do not change here |
| show_logout_button | bool | auto | Auto-set to true when enable_login is true |
| show_quota | bool | false | Show a storage usage bar below the nav. Fetches quota via /{n}:quota on page load. Disabled by default. |
player_config
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| player | string | "videojs" | Video player engine: "videojs", "plyr", "dplayer", "jwplayer" |
| videojs_version | string | "8.12.0" | Video.js CDN version to load |
| plyr_io_version | string | "3.7.8" | Plyr CDN version to load |
| jwplayer_version | string | "8.16.2" | JWPlayer version (requires a valid JWPlayer license) |
Multiple Drives
Add one entry per drive/folder to the roots array. Each gets its own numbered path (/0:/, /1:/, etc.) and appears as a tile on the homepage.
"roots": [
{
"id": "root",
"name": "Personal Drive",
"protect_file_link": false
},
{
"id": "0AOM2i7Mi3uWIUk9PVA",
"name": "Team Shared Drive",
"protect_file_link": false
},
{
"id": "1A2B3C4D5E6F7G8H9I0J1K2L3M",
"name": "Archive (Login Required)",
"protect_file_link": true
}
]Finding a Drive ID:
- My Drive: Use
"root"- Shared Drive: Open in Google Drive → the URL contains
.../drive/folders/DRIVE_ID- Specific Folder: Open the folder → copy the ID from the URL
...folders/FOLDER_IDNote: Folder IDs (not Shared Drive IDs) will not have search working correctly — Google's API only supports full-drive search, not folder-scoped search without a driveId.
Per-Drive Credentials
Each entry in the roots array can have its own OAuth or service account credentials. When per-drive credentials are present, that drive uses them independently — no shared token, no quota collision.
If a drive does not have its own credentials, it falls back to the global client_id / client_secret / refresh_token (or the randomly selected serviceaccounts[] entry).
"roots": [
{
"id": "root",
"name": "Personal Drive",
"protect_file_link": false
// No per-drive creds → uses global OAuth credentials
},
{
"id": "0ABCDEFGabcdefg",
"name": "Company Shared Drive",
"protect_file_link": false,
// Per-drive OAuth credentials
"client_id": "company-client-id.apps.googleusercontent.com",
"client_secret": "GOCSPX-company-secret",
"refresh_token": "1//company-refresh-token"
},
{
"id": "0XYZxyzXYZxyz",
"name": "Archive (Service Account)",
"protect_file_link": true,
// Per-drive service account
"service_account": true,
"service_account_json": {
"type": "service_account",
"project_id": "archive-project",
"private_key_id": "abc123",
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "...",
"token_uri": "https://oauth2.googleapis.com/token"
}
}
]How it works internally:
- Each
googleDriveinstance checks its ownrootconfig for credentials on every token refresh - Tokens are cached per-drive instance with a 58-minute TTL (slightly under Google's 60-minute expiry)
- If per-drive creds are missing or incomplete, the global
getAccessToken()is called as the fallback
Cross-Drive ID Lookup
GDI can automatically find a file or folder by its raw Google Drive ID across all configured drives, without you needing to know which drive it belongs to.
GET /?driveid=GOOGLE_DRIVE_ID
Searches all configured drives for the given ID and redirects to the file's path in the index.
https://your-index.workers.dev/?driveid=1PivBPUBk8Nz6kpQIuJFfa8VeiqQJHoxn
https://your-index.workers.dev/?driveid=1PivBPUBk8Nz6kpQIuJFfa8VeiqQJHoxn&view=true| Parameter | Required | Description |
|-----------|----------|-------------|
| driveid | Yes | A raw (unencrypted) Google Drive file or folder ID |
| view | No | Set to true to open in viewer mode (?a=view) |
Redirect flow:
- Tries
findPathById()on each configured drive in order - If found →
302redirect to/{driveIndex}:/path/to/item - If not in any drive hierarchy but credentials can access it →
302redirect to/fallback?id=... - If not found anywhere →
404JSON error
GET /findpath?id=GOOGLE_DRIVE_ID
Same cross-drive search as above but designed for external integrations (used by other apps to resolve a raw Drive ID into a browseable URL).
https://your-index.workers.dev/findpath?id=1PivBPUBk8Nz6kpQIuJFfa8VeiqQJHoxn
https://your-index.workers.dev/findpath?id=1PivBPUBk8Nz6kpQIuJFfa8VeiqQJHoxn&view=trueRedirect behavior is identical to /?driveid= above.
GET /{driveIndex}:findpath?id=GOOGLE_DRIVE_ID
Like /findpath but starts with a specific drive, then falls through to all others if not found.
https://your-index.workers.dev/1:findpath?id=1PivBPUBk8Nz6kpQIuJFfa8VeiqQJHoxnUseful when you know the file is likely in drive 1 but want automatic fallback to other drives.
Service Account Setup
Service accounts allow you to serve a Shared Drive without an OAuth refresh token. This is ideal for public indexes or team deployments.
Step-by-step
- Google Cloud Console → IAM & Admin → Service Accounts → Create Service Account
- Give it a name and click through
- Click the new service account → Keys → Add Key → Create new key → JSON
- Download the
.jsonfile - Share the Google Drive/folder with the service account's email address (shown in the JSON as
client_email) — grant it Viewer access - Paste the JSON into
serviceaccounts:
const serviceaccounts = [
{
"type": "service_account",
"project_id": "my-project-123",
"private_key_id": "abc123def456",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvg...\n-----END PRIVATE KEY-----\n",
"client_email": "[email protected]",
"client_id": "123456789012345678901",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
];- In
authConfig, set:
"service_account": true,Multiple Service Accounts (Load Balancing)
Add multiple service account objects to the array. One is selected randomly on each worker cold-start, distributing API quota across accounts:
const serviceaccounts = [
{ /* service account 1 */ },
{ /* service account 2 */ },
{ /* service account 3 */ }
];Login & Authentication
Set "enable_login": true in authConfig to protect the entire index. Users must log in to browse files.
Local Username/Password
The simplest setup — credentials stored directly in worker.js:
"login_database": "Local",
"users_list": [
{ "username": "alice", "password": "securePassword1!" },
{ "username": "bob", "password": "anotherPassword2@" }
]Security: Change these from the defaults before deploying. Consider using the KV database for production so credentials are not visible in source code.
Google OAuth Login (Social)
Allow users to sign in with their Google account. Only Google accounts listed in users_list (by email) or present in KV are granted access.
Setup
- In Google Cloud Console → Credentials → Create OAuth 2.0 Client ID
- Application type: Web application
- Authorised redirect URIs:
https://your-worker.workers.dev/google_callback
- Configure
authConfig:
"enable_social_login": true,
"google_client_id_for_login": "123456789-abc.apps.googleusercontent.com",
"google_client_secret_for_login": "GOCSPX-xxxxxxxxxxxxxxxxx",
"redirect_domain": "https://your-worker.workers.dev",
"login_database": "Local",
"users_list": [
{ "username": "[email protected]", "password": "" }
]When using Google login with the Local database, the username field must be the user's full Google email address. The password field is ignored for OAuth users.
Cloudflare KV User Database
For dynamic user management without redeploying the worker, use Cloudflare KV.
Setup
- In Cloudflare Dashboard → Workers & Pages → KV → Create a namespace (e.g.
GDI_USERS) - In your
wrangler.toml:
kv_namespaces = [
{ binding = "ENV", id = "your-kv-namespace-id" }
]- Set in
authConfig:
"login_database": "KV"Add users via Cloudflare Dashboard → KV → your namespace → Add entry:
- Key:
username(or email for Google OAuth) - Value:
password(plaintext — KV is encrypted at rest)
- Key:
For single-session and IP-lock features, the same KV namespace is also used to store session tokens and IP addresses.
Session Security Options
| Option | Config Key | Description |
|--------|-----------|-------------|
| Session duration | login_days: 7 | How many days before the session cookie expires |
| Single session | single_session: true | Logging in on a second device invalidates the first session. Requires KV. |
| IP change logout | ip_changed_action: true | If the user's IP changes after login, they are automatically logged out. Requires KV. |
| IP-locked downloads | enable_ip_lock: true | Download links are bound to the IP that generated them. |
Per-Folder Password Protection
You can password-protect any subfolder without enabling the full login system.
- Set
"enable_password_file_verify": trueinauthConfig - Create a file named
.passwordinside the Google Drive folder you want to protect - Set the file contents to your desired password (plain text, one password per file)
The .password file is never shown in the listing and its contents are never exposed to the browser. Users are prompted to enter the password when they first open the folder — it is cached in localStorage for convenience.
Note: This feature is currently in preview. It protects the listing but not direct download links if
protect_file_linkisfalse.
Encryption Keys
GDI encrypts session cookies and download links using AES-CBC + HMAC-SHA256. The default keys in the source code are public and must be changed before deploying.
Generate random keys:
# 32-byte AES key (64 hex characters)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# 64-byte HMAC key (128 hex characters)
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"Then set them in src/worker.js:
const crypto_base_key = "a1b2c3d4e5f6..."; // 64 hex chars
const hmac_base_key = "f6e5d4c3b2a1..."; // 128 hex charsOr generate them directly in the browser console:
// AES key
crypto.getRandomValues(new Uint8Array(32)).reduce((h,b)=>h+b.toString(16).padStart(2,'0'),'')
// HMAC key
crypto.getRandomValues(new Uint8Array(64)).reduce((h,b)=>h+b.toString(16).padStart(2,'0'),'')Legacy links: GDI v2.4.0+ uses a random IV prepended to each ciphertext. A static fallback IV (
legacy_encrypt_iv) is kept for backward-compatible decryption of links generated before v2.4.0. Do not changelegacy_encrypt_iv.
Download URL Protection
All download links go through /download.aspx with the following protections:
- Encrypted file ID — the real Google Drive file ID is never exposed
- Expiring links — links expire after
file_link_expirydays (default: 7) - HMAC integrity — prevents link tampering (changing the file ID or expiry)
- IP lock (optional) — the link is only valid from the IP that generated it
- Login required (optional) —
disable_anonymous_download: trueblocks all downloads without a session
How a download link works
/download.aspx
?file=<AES-encrypted file ID>
&expiry=<AES-encrypted Unix ms timestamp>
&mac=<HMAC-SHA256 of "fileId|expiry" or "fileId|expiry|ip">
[&ip=<AES-encrypted IP>] ← only when enable_ip_lock is true
[&inline=true] ← serve inline instead of as attachmentThe worker decrypts the file ID, verifies the MAC, checks the expiry, then streams from Google Drive's API directly to the client.
Region & ASN Blocking
Block access from specific countries or autonomous systems (data centres, VPNs, etc.).
// Block by country code (ISO 3166-1 alpha-2)
const blocked_region = ['CN', 'RU', 'KP'];
// Block by ASN number — see bgplookingglass.com for ASN lists
const blocked_asn = [16509, 14618]; // Example: AWS ASNsBlocked visitors receive a plain "Access Denied" HTML response (status 403).
Load Balancing (Multiple Download Domains)
You can distribute download traffic across multiple Cloudflare Worker deployments.
const domains_for_dl = [
'https://dl1.yourworker.workers.dev',
'https://dl2.yourworker.workers.dev',
'https://dl3.yourworker.workers.dev'
];In uiConfig:
"second_domain_for_dl": trueEach domain must be a separate Cloudflare Worker deployment of worker.js (can be the same code). One is picked randomly per request.
Warning: When
second_domain_for_dlistrue, the worker servesdisable_download.htmlfor direct download requests. Downloads are served exclusively through the secondary domain list.
Themes
GDI uses Bootswatch themes on top of Bootstrap 5. Set the theme key in uiConfig:
"theme": "darkly" // dark theme (default)
"theme": "flatly" // clean light theme
"theme": "cyborg" // high-contrast dark
"theme": "vapor" // neon dark
"theme": "quartz" // glassmorphism lightThe user can also toggle dark/light mode manually via the moon/sun icon in the navbar. Their preference is saved in localStorage.
Available themes:
cerulean · cosmo · cyborg · darkly · flatly · journal · litera · lumen · lux · materia · minty · morph · pulse · quartz · sandstone · simplex · sketchy · slate · solar · spacelab · superhero · united · vapor · yeti · zephyr
Media Players
Video
Configure the player engine in player_config:
const player_config = {
"player": "videojs" // "videojs" | "plyr" | "dplayer" | "jwplayer"
};| Player | HLS support | Keyboard shortcuts | Notes |
|--------|------------|-------------------|-------|
| videojs | Yes | Space/F/M/←/→/↑/↓ | Default. Best all-round |
| plyr | Yes (with plugin) | Yes | Minimal, beautiful UI |
| dplayer | Yes | Yes | Danmu/comment support |
| jwplayer | Yes | Yes | Commercial licence required |
Supported formats: MP4, WebM, AVI, MKV, MOV, FLV, TS, 3GP, M4V, RMVB, and more.
To disable all players and serve files directly: "disable_player": true
Audio
Uses APlayer (auto-loaded). When opening a single audio file, GDI automatically fetches the folder's other audio files and builds a playlist.
Supported formats: MP3, FLAC, WAV, OGG, M4A, AAC, WMA, ALAC
To hide the download button on audio: "disable_audio_download": true
Uses PDF.js (auto-loaded). Features:
- Page-by-page navigation with Previous/Next buttons
- Zoom slider (50%–200%)
- Full download button
Images
Direct <img> display with lazy loading. Supported: JPG, JPEG, PNG, GIF, BMP, SVG, TIFF, ICO.
Code / Text
In-browser code display for files up to 2 MB. Supported: PHP, CSS, Go, Java, JS, JSON, TXT, SH, MD, HTML, XML, Python, Ruby, C, C++, H, HPP.
Search
Type in the search box to search across your drives.
- Current drive only: Set
"search_all_drives": falseinauthConfig - All drives: Set
"search_all_drives": true(default) - Results show in an infinite-scroll list with folder/file icons
- Clicking a folder or file in search results opens a modal with a direct link to the item's path (resolved via the Google Drive API)
- If path resolution fails, a
/fallback?id=...link is shown which always works
Search limitations:
- Folder IDs (as opposed to Shared Drive IDs) do not support drive-scoped search via the Google API — search will fall back to user/all drives
- Special characters
" ' = < > / \ :are stripped from queries
Deployment
Via Cloudflare Dashboard (No CLI)
- Go to dash.cloudflare.com
- Workers & Pages → Create Application → Create Worker
- Click Edit Code (or paste via the online editor)
- Paste the full contents of
src/worker.js - Click Save and Deploy
- Visit your worker URL (e.g.
https://your-worker.your-subdomain.workers.dev)
Via Wrangler CLI
# Install dependencies
npm install
# Login to Cloudflare
npx wrangler login
# Deploy
npm run deploy
# or
npx wrangler deploy src/worker.js --name my-drive-index --compatibility-date 2024-01-01Custom domain:
In Cloudflare Dashboard → Workers & Pages → your worker → Custom Domains → Add Custom Domain — point any domain/subdomain in your Cloudflare DNS.
Environment: production vs development vs local
const environment = 'production'; // Loads assets from jsDelivr CDN
// const environment = 'development'; // Loads from /src/app.min.js (local wrangler dev)
// const environment = 'local'; // Loads from http://127.0.0.1:5500/src/app.js (live reload)Development Setup
Prerequisites
node --version # 18+
npm --version # 8+Install and Run
git clone https://gitlab.com/GoogleDriveIndex/Google-Drive-Index.git
cd Google-Drive-Index
npm install
# Start local dev server (hot-reload for worker, static for assets)
npm run devThe dev server runs on http://localhost:8787 by default.
Code Quality
npm run lint # Check with ESLint
npm run lint:fix # Auto-fix lint errors
npm run format # Format with Prettier
npm run typecheck # TypeScript type-check (no emit)Local Asset Development
For faster frontend iteration, set environment = 'local' in worker.js and open src/app.js with a live-reload server (e.g. VS Code Live Server on port 5500).
Build Process (CDN Assets)
GDI's frontend assets (CSS, JS, images) are bundled inside the npm package and served from jsDelivr via the npm CDN:
https://cdn.jsdelivr.net/npm/@googledrive/index@{version}/All static files (src/app.min.js, assets/gdi.min.css, assets/homepage.min.js, images/, sw.js) live in this repo and are published to npm with each release. No separate CDN repository is needed.
To build and release:
npm run build # Patch CDN_VERSION, minify JS/CSS, output built files into repo
git add src/app.min.js assets/gdi.min.css assets/homepage.min.js sw.js
git commit -m "Release v2.5.7"
git tag v2.5.7 && git push && git push --tags
npm publish --access publicWhat npm run build does:
- Reads version from
package.jsonand patchesCDN_VERSIONinsrc/worker.jsandGDI_VERSIONingenerator/worker.js - Minifies
src/app.js→src/app.min.jsusing esbuild - Minifies
assets/homepage.js→assets/homepage.min.js - Minifies
assets/gdi.css→assets/gdi.min.css
After publishing, jsDelivr picks up the new version within minutes — no tag propagation delay.
API Reference
These are the internal POST/GET API endpoints the frontend uses. All paths are relative to your worker URL.
POST /{driveIndex}:/path/to/folder/
Fetch file listing for a directory.
Request body (JSON):
{
"id": "",
"type": "folder",
"password": "",
"page_token": "",
"page_index": 0
}Response (JSON):
{
"nextPageToken": "token_or_null",
"curPageIndex": 0,
"data": {
"files": [
{
"id": "<encrypted>",
"name": "file.mp4",
"mimeType": "video/mp4",
"size": "104857600",
"modifiedTime": "2024-01-15T10:30:00.000Z",
"fileExtension": "mp4",
"driveId": "<encrypted>",
"link": "/download.aspx?file=...&expiry=...&mac=..."
}
]
}
}POST /{driveIndex}:search
Full-text search.
Request body (JSON):
{ "q": "search terms", "page_token": null, "page_index": 0 }Response: Same structure as file listing.
POST /{driveIndex}:id2path
Resolve an encrypted file/folder ID to its path.
Request body (JSON):
{ "id": "<encrypted file ID>" }Response (JSON):
{ "path": "/0:/folder/subfolder/file.mp4" }GET /download.aspx
Stream or download a file. For Google Workspace files, uses the Drive export API automatically.
| Parameter | Description |
|-----------|-------------|
| file | AES-encrypted file ID |
| expiry | AES-encrypted Unix ms expiry timestamp |
| mac | HMAC-SHA256 integrity token |
| ip | AES-encrypted IP (only when enable_ip_lock: true) |
| inline | "true" to serve inline instead of as attachment |
| fmt | Export format extension for Google Workspace files: pdf, docx, txt (Docs), xlsx, csv (Sheets), pptx (Slides). Defaults to pdf if omitted. |
GET /{driveIndex}:quota
Returns storage quota for the drive's Google account. Requires show_quota: true in uiConfig to be surfaced in the UI (but the endpoint is always available).
Response (JSON):
{
"user": { "displayName": "...", "emailAddress": "..." },
"storageQuota": {
"limit": "16106127360",
"usage": "4831838208",
"usageInDrive": "1234567890",
"usageInDriveTrash": "0"
}
}POST /login
Authenticate with username/password.
Request body: application/x-www-form-urlencoded with username and password.
Response (JSON):
{ "ok": true, "redirect": "/" }
// or
{ "ok": false, "message": "Invalid username or password." }GET /logout
Clears the session cookie and redirects to /login.
GET /google_callback
OAuth callback handler. Exchanges the Google authorization code for an ID token, validates the email against the user list, and sets a session cookie.
GET /findpath
Cross-drive file/folder lookup by raw Google Drive ID. Searches all configured drives and redirects to the resolved path.
| Parameter | Required | Description |
|-----------|----------|-------------|
| id | Yes | Raw (unencrypted) Google Drive file or folder ID |
| view | No | "true" to redirect to viewer mode |
Response: 302 redirect to /{driveIndex}:/path (found in drive), 302 to /fallback?id=... (accessible via credentials but not in any drive root), or 404 JSON if not found anywhere.
GET /{driveIndex}:findpath
Same as /findpath but starts the search from a specific drive index. Falls through to all other drives if not found in the specified one.
| Parameter | Required | Description |
|-----------|----------|-------------|
| id | Yes | Raw (unencrypted) Google Drive file or folder ID |
| view | No | "true" to redirect to viewer mode |
GET /?driveid=
Cross-drive lookup starting from the homepage. See Cross-Drive ID Lookup.
POST /{driveIndex}:fallback
Resolve an encrypted file/folder ID from an external source (used by the fallback page to show files/folders that are accessible via credentials but are not rooted in any configured drive).
Request body (JSON):
{ "id": "<encrypted ID>", "type": "folder", "page_token": null, "page_index": 0 }- Omit
"type"(or set to any non-"folder"value) to look up a single file - Include
"type": "folder"to list a folder's contents
Response: File metadata JSON (single file) or listing JSON (folder).
GET /{driveIndex}:findpath → POST /{driveIndex}:id2path
Internal pair used by the search result modal to resolve encrypted IDs to paths.
id2path takes { "id": "<encrypted>" } and returns { "path": "/0:/folder/file.mp4" } or { "path": null } (404) if not resolvable.
POST /copy
Copy a file within Google Drive.
Request body: application/x-www-form-urlencoded
| Field | Description |
|-------|-------------|
| id | Encrypted file ID (as returned by the listing API) |
| root_id | Raw Google Drive folder ID where the copy should be placed |
Response (JSON): The created file metadata from the Google Drive API, or an error object.
GET /sw.js
Serves the service worker script for offline support (fetched from CDN). Non-critical — browsing continues normally if this fails.
Troubleshooting / FAQ
Getting a Refresh Token Manually
If the generator tool is unavailable, get a refresh token manually:
- In Google Cloud Console, create an OAuth 2.0 Web App credential
- Set the redirect URI to
https://developers.google.com/oauthplayground - Go to OAuth Playground
- Click the settings gear → check "Use your own OAuth credentials" → enter your client ID and secret
- In Step 1, select
https://www.googleapis.com/auth/drive→ Authorize - In Step 2, click "Exchange authorization code for tokens"
- Copy the
refresh_tokenvalue
"Invalid Request!" on downloads
The download link has either expired, been tampered with, or your IP has changed (if enable_ip_lock is enabled). Generate a new link by refreshing the file listing page.
Files not showing up
- Verify the drive ID is correct and the account/service account has been granted access
- Google Docs, Sheets, and Slides now appear in the listing and can be exported to PDF/DOCX/XLSX/PPTX/TXT/CSV. Google Forms and Sites are still excluded (cannot be exported via the Drive API).
- Check if
.passwordfile protection is enabled and a password is set on the folder
Search not working with a Folder ID
If you set a folder ID (not a Shared Drive ID) in roots, the Google Drive API does not support drive-scoped search for regular folders. Use "search_all_drives": true or set a proper Shared Drive ID.
Login page keeps looping
- Ensure your encryption keys are set correctly (not the default public keys)
- If using KV, ensure the
ENVKV binding is created and bound inwrangler.toml - Check that
redirect_domaindoes not have a trailing slash
"User Logged in Someplace Else" error
single_session: true is enabled and another device/browser has logged in. Log out everywhere and sign in again on one device only.
Worker exceeds CPU time limit
- The free tier allows 10ms CPU per request. Most requests are I/O-bound (waiting on Google API) so this is rarely hit
- If it occurs, consider upgrading to Cloudflare Workers Paid plan (50ms CPU limit) or optimising
files_list_page_size
Console shows "Report this page when asked..."
This is the global error handler. Copy the full error message and open an issue on the repository with:
- The error text
- Your
authConfig(with credentials removed) - The request that caused the error
CORS errors on downloads
Set "enable_cors_file_down": true in authConfig to add Access-Control-Allow-Origin: * to all download responses.
Service worker registration fails
This is a non-critical error. The /sw.js endpoint fetches a service worker from the CDN for offline support. If the CDN is unreachable, the error is caught silently and browsing continues normally.
Planned Features
The following features are under consideration for future releases:
- [ ] Rate limiting on the login endpoint to prevent brute-force attacks
- [ ] Bulk download as ZIP — select multiple files and download a ZIP archive
- [ ] File upload — upload files to Google Drive from the web UI (requires write permission)
- [ ] Thumbnail/grid view — image gallery mode for photo folders
- [ ] Subtitle support — auto-detect
.srt/.vttfiles for video player subtitles - [ ] Admin panel — manage KV users, view access logs
- [ ] Analytics — optional lightweight visit/download counters
- [ ] Custom folder sorting — pin folders to top, custom order via metadata files
- [ ] Webhook on download — notify a URL when a file is downloaded
- [ ] Password reset flow — for KV-based users, email-based password reset
- [ ] Two-factor authentication — TOTP/HOTP for the login system
- [ ] Embed mode —
?embed=1already supported; iframe-friendly minimal UI - [ ] MongoDB user database —
login_database: "MongoDB"placeholder already in code
Changelog
v2.5.8 (Current)
Bug fixes:
- Fixed: File size not shown on mobile in search results — added
data-sizeattribute to the search result row-name element so the CSS pseudo-element rule can render it below the filename at ≤560px viewport width.
v2.5.7
Bug fixes:
- Fixed: Mobile search bar was hidden at ≤420px viewport width — removed
display:noneon.gdi-nav-search, replaced with narrowedmax-widthand hidden separator instead. - Fixed: Breadcrumb showed raw URL segment (e.g.
2:) instead of the configured drive name — now maps throughwindow.drive_names[]in bothgenerateBreadcrumb()andlist(). - Fixed: File viewer crash ("Cannot set properties of null") when
requestListPath()was called from a file viewer page that has no#update/#listelements — switched to jQuery$('#id').html()which silently no-ops on missing elements. - Fixed: "More options" dropdown in the file viewer was clipped behind the viewer card — removed
overflow: hiddenfrom.gdi-viewer-cardand.gdi-btn-split, added border-radius to first/last children instead.
UI:
- Sticky footer — debug bar and footer now pin to the bottom of the viewport on short pages and naturally follow content on long pages (
body { display:flex; flex-direction:column }+#content { flex:1 }).
v2.5.6
Bug fixes:
- Fixed: Search result click for files in unconfigured shared drives (
rootIdx = -2) navigated to/fallback?id=...but failed with 400 —getQueryVariablereturned the URL-encoded ID (%2Binstead of+), causingdecryptStringto fail. Fixed withdecodeURIComponent.
CDN / release:
- CDN migrated from separate GitHub CDN repo to npm package (
@googledrive/index) — assets now served viacdn.jsdelivr.net/npm/@googledrive/index@{version}/. No separate CDN repository needed. CDN_VERSIONreplacesCDN_SHA— single version string drives all CDN URLs; auto-patched bynpm run buildfrompackage.json.- Added
filesfield topackage.jsonso only deployable files are included in the npm package. npm run buildnow patches version strings, minifies CSS/JS, and prepares the repo fornpm publish.
v2.5.5
New features:
- New: Google Workspace export — Google Docs, Sheets, and Slides now appear in file listings and can be exported via
/download.aspx?fmt=<ext>. Supported formats: PDF, DOCX, TXT (Docs); PDF, XLSX, CSV (Sheets); PDF, PPTX (Slides). Google Forms and Sites remain excluded. - New: Storage quota display —
show_quota: trueinuiConfigshows a usage bar below the nav (green/orange/red). Backed by a newGET /{n}:quotaendpoint (about.get). Disabled by default. - New: Auto-discover Shared Drives in the generator — paste a temporary access token, click "Fetch Drives", and check the drives you want to add. Supports "Select All" and closes the panel automatically after adding.
- New:
GET /{n}:quota— returnsstorageQuotaanduserfrom the Driveabout.getAPI for the drive's account - New:
GET /findpath?id=— cross-drive file/folder lookup; searches all configured drives in order, falls back to/fallbackif accessible via credentials but not in any drive root, returns 404 if not found anywhere - New:
GET /?driveid=DRIVE_ID— same cross-drive lookup from the homepage; supports&view=true - New:
GET /{n}:findpath?id=— per-drive findpath that falls through to all other drives automatically - New: Per-drive credentials — each
roots[]entry can have its ownclient_id/client_secret/refresh_tokenorservice_account/service_account_json; falls back to global credentials if not set - New:
POST /copy— copy a Google Drive file to a specified folder via the worker API
Bug fixes:
- Fixed:
GET /findpathpreviously hard-redirected to/0:findpath, meaning it only ever searched drive 0 - Fixed:
findId2Path(used by/{n}:findpath) only tried one drive and never fell back to others - Fixed:
/?driveid=redirected to/fallbackeven for IDs that don't exist anywhere (now returns 404) - Fixed: OAuth error redirect pointed to
/?error=Invalid Tokeninstead of/login?error=Invalid+Token - Fixed:
handleSearchresponses were missingContent-Type: application/jsonheader - Fixed:
handleId2Pathreturned{"path":"/undefined:undefined"}when path was not resolvable - Fixed:
_list_gdrive_filesreturnednullwhen parent ID was undefined, causingTypeErrorin callers - Fixed: Google API error responses (no
filesarray) caused crashes in listing code - Fixed: Fallback URL had unencoded base64 characters (
+,=,/) in theidquery param - Fixed: Fallback file response was missing
Content-Typeheader - Fixed:
/3:/{nonexistent}(and similar) API returned 500 instead of 404 when file not found - Fixed: Breadcrumb incorrectly truncated folder names containing
?(e.g. "My Folder? Yes" became "My Folder") - Fixed:
findItemByIdconditionally omitted&supportsAllDrives=trueon user drives, causing lookup failures for shared items - Fixed:
redirectToIndexPage()used HTTP 307 (method-preserving); redirected POST/loginre-sent the form body to/0:/, causing a 500 error when login is disabled — changed to 302 - Fixed:
handleId2Pathreturned 500 for requests with invalid base64 in the encrypted ID (now returns 400 "Invalid encrypted ID") - Fixed:
findParentFilesRecursioncompared parent IDs against the string"root"instead of the real Google Drive root folder ID (target_top_id), causingPOST /{n}:id2pathto always return{ "path": null }for files in personal drives - Fixed:
fetchAccessTokendid not checkresponse.ok— non-2xx token responses were silently swallowed; now throws with the HTTP status - Fixed:
get_single_file_api,searchFilesinDrive,findItemById,_findDirIdall lackedresponse.okchecks — API errors could cause crashes or silent empty results; all now return safe fallback values on non-2xx - Fixed:
get_single_filecalleddownload(file.id)without checking iffileorfile.idwas null first; now returns 404 if the file is not found - Fixed:
console.logof raw file ID and download path left in production code path; removed
v2.4.1
- Fixed:
sleep()in download retry loop was notawait-ed (no actual delay between retries) - Fixed: POST API requests bypassed session authentication when
enable_login: true - Fixed:
kv_keywasundefinedin Google OAuth callback for local-database users (broken session creation → login loop) - Fixed:
params.get("q")crash when navigating to/searchwith noqquery parameter - Fixed:
path.slice(3)stripped wrong number of characters for drive indexes > 9 (drives 10+) - Fixed:
details.parents[0] = nullthrew ifparentswas undefined or empty - Fixed:
Access-Control-Allow-Credentialsheader set to booleantrueinstead of string"true" - Fixed: Logout redirected to
/?error=...instead of/loginwhen login is enabled - Fixed: Session cookie not cleared with
Max-Age=0on logout; path was missing - Fixed:
var user_foundscoping — replaced all withlet user_found = falseto prevent undefined reference - Fixed: Frontend
sleep()was a CPU-blocking busy-wait loop (froze browser UI during retry) - Fixed:
performRequest()inrequestListPathnever decremented the retry counter (potential infinite retry loop) - Fixed:
requestSearch()retry had the same infinite-retry bug - Fixed:
data-bytesattribute in fallback list and search results wasNaNafterformatFileSize()conversion (broke column sorting by size) - Fixed:
id2pathfetch sent wrongContent-Type: application/x-www-form-urlencodedheader for a JSON body - Fixed:
MutationObserverondocumentElementadded a new click listener to select-all checkbox on every DOM mutation (memory leak + multiple listener bug) - Fixed:
fallback = trueused as function argument (assignment expression, not a value) — replaced with literaltrue - Fixed:
formatFileSize()returned''for 0-byte files; now returns"0 bytes" - Improved: Session validation now returns proper JSON 401 responses for POST API requests (instead of HTML login page)
v2.4.0
- New: Random IV per encryption operation (AES-CBC) for session cookies and download links
- New: Legacy static IV kept as fallback for pre-v2.4.0 links
- New: Redesigned login page with password toggle and Google OAuth button
- New:
gdi.cssv2.5.0 design system with CSS custom properties for theming - New: Homepage grid with search bar
- New: Toast notifications for clipboard copy
- New: PDF.js viewer with zoom/navigation
- New: APlayer audio player with auto-playlist detection
- Improved: Retry logic with exponential back-off across all API calls
v2.3.x
- Multiple drives with homepage grid
- Bootstrap 5 migration
- Dark/light theme toggle
- Bulk file selection
Credits
- Original concept: maple3142/GDIndex and yanzai/goindex
- Author / Maintainer: Parveen Bhadoo — @PBhadoo
- UI Redesign (v2.5.0): TheFirstSpeedster
- UI Framework: Bootstrap 5 + Bootswatch + Bootstrap Icons
- API: Google Drive API v3
- Video: Video.js, Plyr, DPlayer
- Audio: APlayer
- PDF: PDF.js by Mozilla
- Markdown: Marked.js
- CDN: jsDelivr
- Hosting: Cloudflare Workers
Sponsors
Support the project:
License
MIT License — Copyright (c) Parveen Bhadoo

