GistifyDeveloper Gists
Explore Code
New Snippet

The minimalist platform to share code snippets, manage gists, and customize your developer identity.

Platform

  • New Snippet
  • Explore Gists
  • Profile Dashboard

Legal & Privacy

  • Privacy Policy
  • Terms of Service
  • Cookie Policy

Developer

  • GitHub @kyiov
  • Telegram Support
  • Saweria
© 2026 GistifyAll rights reserved.

Crafted with by kyio

Back to Home
Gistify Snippet
kyiov
@kyiov
1h ago·1 file(s)
View profile
Public

Google Drive & Docs Direct Download Link Generator

Zero-dependency Google Drive, Docs, Sheets, and Slides direct download URL generator with format conversion (PDF, DOCX, XLSX) and link status verification.

#gdrive#googledrive#downloader#tools#javascript#api
0
Raw
gdrive.jsjavascript
361 lines
1// gdrive.js — bundle standalone. npm deps: node:fs, node:fs/promises, node:path, node:stream, node:stream/promises, node:url
2
3/**
4 * Scraper: gdrive.js
5 * Source : mithra/gdrive.js
6 *
7 * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object)
8 * Output : Promise<Object | Array | Buffer> (JSON formatted scraping response / stream URL / media metadata)
9 *
10 * CLI Run:
11 * node mithra/gdrive.js [functionName] [args...]
12 * node cli.js mithra/gdrive.js [functionName] [args...]
13 */
14
15import * as __ns_585 from 'node:fs'
16const statSync = __ns_585.statSync
17const createWriteStream = __ns_585.createWriteStream
18import * as __ns_586 from 'node:fs/promises'
19const fs = __ns_586.default
20import * as __ns_587 from 'node:path'
21const path = __ns_587.default
22import * as __ns_588 from 'node:stream'
23const Readable = __ns_588.Readable
24import * as __ns_589 from 'node:stream/promises'
25const pipeline = __ns_589.pipeline
26import * as __ns_590 from 'node:url'
27const pathToFileURL = __ns_590.pathToFileURL
28
29/**
30 * Google Drive / Google Docs direct download link generator.
31 *
32 * Logic diambil dari vinish.dev/google-drive-file-downloader (client-side,
33 * tanpa server API). Zero npm dependency, Node >= 20 (built-in fetch).
34 *
35 * TEST LANGSUNG:
36 * node gdrive.js https://drive.google.com/file/d/<id>/view
37 * node gdrive.js <fileId>
38 * node gdrive.js https://docs.google.com/document/d/<id>/edit --format pdf
39 * node gdrive.js <url> --check
40 * node gdrive.js <url> --download [outputPath]
41 */
42
43const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36'
44
45const MIME_EXT = {
46 'application/pdf': '.pdf',
47 'text/plain': '.txt',
48 'text/csv': '.csv',
49 'text/html': '.html',
50 'application/json': '.json',
51 'image/png': '.png',
52 'image/jpeg': '.jpg',
53 'image/gif': '.gif',
54 'image/webp': '.webp',
55 'image/svg+xml': '.svg',
56 'application/zip': '.zip',
57 'application/x-zip-compressed': '.zip',
58 'application/gzip': '.gz',
59 'application/x-tar': '.tar',
60 'application/vnd.rar': '.rar',
61 'application/x-7z-compressed': '.7z',
62 'application/msword': '.doc',
63 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
64 'application/vnd.ms-excel': '.xls',
65 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
66 'application/vnd.ms-powerpoint': '.ppt',
67 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
68 'application/vnd.oasis.opendocument.text': '.odt',
69 'application/vnd.oasis.opendocument.spreadsheet': '.ods',
70 'application/vnd.oasis.opendocument.presentation': '.odp',
71 'audio/mpeg': '.mp3',
72 'audio/mp4': '.m4a',
73 'audio/wav': '.wav',
74 'audio/ogg': '.ogg',
75 'video/mp4': '.mp4',
76 'video/webm': '.webm',
77 'video/x-matroska': '.mkv',
78 'video/quicktime': '.mov',
79}
80
81const TYPES = {
82 file: {
83 name: 'Google Drive File',
84 formats: [],
85 defaultFormat: null,
86 url: (id) => `https://drive.google.com/uc?export=download&confirm=t&id=${id}`,
87 },
88 document: {
89 name: 'Google Document',
90 formats: ['pdf', 'docx', 'odt', 'rtf', 'txt', 'html'],
91 defaultFormat: 'pdf',
92 url: (id, fmt) => `https://docs.google.com/document/d/${id}/export?format=${fmt}`,
93 },
94 spreadsheets: {
95 name: 'Google Spreadsheet',
96 formats: ['xlsx', 'pdf', 'ods', 'csv', 'tsv'],
97 defaultFormat: 'xlsx',
98 url: (id, fmt) => `https://docs.google.com/spreadsheets/d/${id}/export?format=${fmt}`,
99 },
100 presentation: {
101 name: 'Google Presentation',
102 formats: ['pptx', 'pdf', 'odp', 'txt'],
103 defaultFormat: 'pptx',
104 url: (id, fmt) => `https://docs.google.com/presentation/d/${id}/export/${fmt}`,
105 },
106 drawing: {
107 name: 'Google Drawing',
108 formats: ['png', 'jpeg', 'svg', 'pdf'],
109 defaultFormat: 'png',
110 url: (id, fmt) => `https://docs.google.com/drawings/d/${id}/export/${fmt}`,
111 },
112 folder: {
113 name: 'Google Drive Folder',
114 formats: [],
115 defaultFormat: null,
116 url: null,
117 },
118}
119
120const URL_PATTERNS = [
121 { regex: /drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/, type: 'file' },
122 { regex: /drive\.google\.com\/open\?id=([a-zA-Z0-9_-]+)/, type: 'file' },
123 { regex: /drive\.google\.com\/uc\?(?:.*&)?id=([a-zA-Z0-9_-]+)/, type: 'file' },
124 { regex: /docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/, type: 'document' },
125 { regex: /docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/, type: 'spreadsheets' },
126 { regex: /docs\.google\.com\/presentation\/d\/([a-zA-Z0-9_-]+)/, type: 'presentation' },
127 { regex: /docs\.google\.com\/drawings\/d\/([a-zA-Z0-9_-]+)/, type: 'drawing' },
128 { regex: /drive\.google\.com\/drive\/folders\/([a-zA-Z0-9_-]+)/, type: 'folder' },
129 { regex: /[?&]id=([a-zA-Z0-9_-]+)/, type: 'file' },
130]
131
132/**
133 * Parse input (URL atau file ID polos) menjadi { fileId, type }.
134 * @returns {{fileId:string, type:string}|null}
135 */
136function parseGoogleUrl(input) {
137 const value = String(input || '').trim()
138 if (!value) return null
139
140 // File ID polos (hanya alfanumerik, strip, underscore) dianggap file Drive.
141 if (/^[a-zA-Z0-9_-]{6,}$/.test(value) && !value.includes('.')) {
142 return { fileId: value, type: 'file' }
143 }
144
145 for (const pattern of URL_PATTERNS) {
146 const match = value.match(pattern.regex)
147 if (match && match[1]) {
148 return { fileId: match[1], type: pattern.type }
149 }
150 }
151 return null
152}
153
154/**
155 * Generate link download langsung.
156 * @returns {{fileId:string, type:string, typeName:string, formats:string[], defaultFormat:string|null, format:string|null, downloadUrl:string|null}}
157 */
158function generateDownloadLink(input, { format } = {}) {
159 const parsed = parseGoogleUrl(input)
160 if (!parsed) throw new Error('Invalid Google Drive/Docs URL. Example: https://drive.google.com/file/d/<id>/view')
161
162 const config = TYPES[parsed.type]
163 if (parsed.type === 'folder') {
164 throw new Error('Folders cannot be downloaded directly. Share individual files instead.')
165 }
166
167 let fmt = format || config.defaultFormat || null
168 if (fmt && config.formats.length && !config.formats.includes(fmt)) {
169 throw new Error(`Invalid format "${fmt}". Available: ${config.formats.join(', ')}`)
170 }
171
172 return {
173 fileId: parsed.fileId,
174 type: parsed.type,
175 typeName: config.name,
176 formats: config.formats,
177 defaultFormat: config.defaultFormat,
178 format: fmt,
179 downloadUrl: config.url(parsed.fileId, fmt),
180 }
181}
182
183/**
184 * Verifikasi link (status + content-type + ukuran). Body dibatalkan setelah header.
185 */
186async function checkLink(url) {
187 const res = await fetch(url, {
188 method: 'GET',
189 redirect: 'follow',
190 headers: { 'User-Agent': UA },
191 signal: AbortSignal.timeout(25000),
192 })
193 const len = res.headers.get('content-length')
194 if (res.body?.cancel) await res.body.cancel().catch(() => {})
195 return {
196 status: res.status,
197 ok: res.ok,
198 contentType: res.headers.get('content-type') || '',
199 size: len ? Number(len) : null,
200 finalUrl: res.url || url,
201 }
202}
203
204// Ambil link download asli dari halaman konfirmasi virus-scan Drive (file besar).
205function extractConfirmLink(html) {
206 const form = /<form[^>]*action="([^"]+)"[^>]*>/.exec(html)
207 if (form && /drive\.(usercontent\.)?google\.com/.test(form[1])) {
208 return form[1].replace(/&amp;/g, '&')
209 }
210 const anchor = /href="(https:\/\/drive\.usercontent\.google\.com\/download[^"]+)"/.exec(html)
211 if (anchor) return anchor[1].replace(/&amp;/g, '&')
212 return null
213}
214
215// Nama file dari content-disposition, fallback ke ekstensi MIME / format.
216function resolveFileName(res, result) {
217 const cd = res.headers.get('content-disposition') || ''
218 const star = /filename\*=UTF-8''([^;]+)/i.exec(cd)
219 if (star) {
220 const name = decodeURIComponent(star[1].trim())
221 if (name) return name
222 }
223 const plain = /filename="?([^";]+)"?/i.exec(cd)
224 if (plain && plain[1]) return plain[1]
225 const mime = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase()
226 const ext = MIME_EXT[mime] || (result.format ? `.${result.format}` : '')
227 return `drive_${result.fileId}${ext}`
228}
229
230function resolveOutputPath(outputPath, fileName) {
231 if (!outputPath) return path.join(process.cwd(), fileName)
232 let isDir = false
233 try { isDir = statSync(outputPath).isDirectory() } catch { /* bukan direktori / belum ada */ }
234 if (isDir || outputPath.endsWith('/') || outputPath.endsWith(path.sep)) {
235 return path.join(outputPath, fileName)
236 }
237 return outputPath
238}
239
240/**
241 * Download file ke disk. Streaming (tanpa buffer penuh di memori).
242 * @returns {Promise<{filePath:string, fileName:string, size:number, contentType:string, url:string, type:string}>}
243 */
244async function downloadFile(input, { format, outputPath, timeout = 120000 } = {}) {
245 const result = generateDownloadLink(input, { format })
246
247 let res = await fetch(result.downloadUrl, {
248 redirect: 'follow',
249 headers: { 'User-Agent': UA },
250 signal: AbortSignal.timeout(timeout),
251 })
252 if (!res.ok) {
253 await res.body?.cancel?.().catch(() => {})
254 throw new Error(`HTTP ${res.status}`)
255 }
256 let finalUrl = res.url || result.downloadUrl
257
258 // File Drive besar: uc mengembalikan halaman konfirmasi virus (text/html).
259 // File non-publik: uc mengembalikan halaman Sign-in.
260 const ctype = (res.headers.get('content-type') || '').toLowerCase()
261 if (ctype.includes('text/html')) {
262 const html = await res.text()
263 if (/sign-in|servicelogin|\/v3\/signin\//i.test(html)) {
264 throw new Error('File tidak publik (butuh login Google). Pastikan file di-share sebagai Anyone with the link.')
265 }
266 const direct = extractConfirmLink(html)
267 if (!direct) {
268 throw new Error('Drive meminta konfirmasi tetapi link download tidak ditemukan.')
269 }
270 res = await fetch(direct, {
271 redirect: 'follow',
272 headers: { 'User-Agent': UA },
273 signal: AbortSignal.timeout(timeout),
274 })
275 if (!res.ok) {
276 await res.body?.cancel?.().catch(() => {})
277 throw new Error(`HTTP ${res.status}`)
278 }
279 finalUrl = res.url || direct
280 }
281
282 const fileName = resolveFileName(res, result)
283 const filePath = resolveOutputPath(outputPath, fileName)
284
285 await fs.mkdir(path.dirname(filePath), { recursive: true })
286 const nodeStream = Readable.fromWeb(res.body)
287 await pipeline(nodeStream, createWriteStream(filePath))
288
289 const stat = await fs.stat(filePath)
290 return {
291 filePath,
292 fileName,
293 size: stat.size,
294 contentType: res.headers.get('content-type') || '',
295 url: finalUrl,
296 type: result.type,
297 }
298}
299
300// ===== SELF-TEST: node gdrive.js <url|id> [--format <fmt>] [--check] [--download <path>] =====
301const isDirectRun = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href
302
303if (isDirectRun) {
304 ;(async () => {
305 const args = process.argv.slice(2)
306 const input = args.find((a) => !a.startsWith('--'))
307 const fmtArg = args.indexOf('--format') >= 0 ? args[args.indexOf('--format') + 1] : undefined
308 const doCheck = args.includes('--check')
309 const dlIdx = args.indexOf('--download')
310 const dlPath = dlIdx >= 0 ? (args[dlIdx + 1] && !args[dlIdx + 1].startsWith('--') ? args[dlIdx + 1] : undefined) : undefined
311 const doDownload = dlIdx >= 0
312
313 if (!input) {
314 console.log('Usage: node gdrive.js <url|fileId> [--format <fmt>] [--check] [--download <path>]')
315 console.log('')
316 console.log('Examples:')
317 console.log(' node gdrive.js https://drive.google.com/file/d/<id>/view')
318 console.log(' node gdrive.js <fileId> --check')
319 console.log(' node gdrive.js https://docs.google.com/document/d/<id>/edit --format pdf')
320 console.log(' node gdrive.js <url> --download ./downloads')
321 console.log(' node gdrive.js <url> --format mp4')
322 process.exit(0)
323 }
324
325 try {
326 const result = generateDownloadLink(input, { format: fmtArg })
327 console.log('== Google Drive downloader ==')
328 console.log(`input : ${input}`)
329 console.log(`type : ${result.type} (${result.typeName})`)
330 console.log(`id : ${result.fileId}`)
331 if (result.formats.length) {
332 console.log(`formats: ${result.formats.join(', ')}`)
333 console.log(`format: ${result.format} (default ${result.defaultFormat})`)
334 }
335 console.log(`url : ${result.downloadUrl}`)
336
337 if (doCheck) {
338 const info = await checkLink(result.downloadUrl)
339 console.log(`check : HTTP ${info.status}${info.ok ? '' : ' (may require confirmation)'} | ${info.contentType || 'unknown'} | ${info.size != null ? `${(info.size / 1048576).toFixed(2)} MB` : 'size unknown'}`)
340 }
341
342 if (doDownload) {
343 const dl = await downloadFile(input, { format: fmtArg, outputPath: dlPath })
344 console.log(`saved : ${dl.filePath}`)
345 console.log(`name : ${dl.fileName}`)
346 console.log(`size : ${(dl.size / 1048576).toFixed(2)} MB (${dl.size} bytes)`)
347 console.log(`type : ${dl.contentType || 'unknown'}`)
348 }
349 console.log('done')
350 } catch (e) {
351 console.error(`ERROR: ${e.message}`)
352 process.exit(1)
353 }
354 })()
355}
356
357export { generateDownloadLink, parseGoogleUrl, checkLink, downloadFile };
358export default generateDownloadLink;
359
360/* Legacy generic self-test intentionally removed; the CLI above is the single entry point. */
361
Discussion & Comments0

No comments yet — be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.