// gdrive.js — bundle standalone. npm deps: node:fs, node:fs/promises, node:path, node:stream, node:stream/promises, node:url /** * Scraper: gdrive.js * Source : mithra/gdrive.js * * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object) * Output : Promise (JSON formatted scraping response / stream URL / media metadata) * * CLI Run: * node mithra/gdrive.js [functionName] [args...] * node cli.js mithra/gdrive.js [functionName] [args...] */ import * as __ns_585 from 'node:fs' const statSync = __ns_585.statSync const createWriteStream = __ns_585.createWriteStream import * as __ns_586 from 'node:fs/promises' const fs = __ns_586.default import * as __ns_587 from 'node:path' const path = __ns_587.default import * as __ns_588 from 'node:stream' const Readable = __ns_588.Readable import * as __ns_589 from 'node:stream/promises' const pipeline = __ns_589.pipeline import * as __ns_590 from 'node:url' const pathToFileURL = __ns_590.pathToFileURL /** * Google Drive / Google Docs direct download link generator. * * Logic diambil dari vinish.dev/google-drive-file-downloader (client-side, * tanpa server API). Zero npm dependency, Node >= 20 (built-in fetch). * * TEST LANGSUNG: * node gdrive.js https://drive.google.com/file/d//view * node gdrive.js * node gdrive.js https://docs.google.com/document/d//edit --format pdf * node gdrive.js --check * node gdrive.js --download [outputPath] */ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36' const MIME_EXT = { 'application/pdf': '.pdf', 'text/plain': '.txt', 'text/csv': '.csv', 'text/html': '.html', 'application/json': '.json', 'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'image/svg+xml': '.svg', 'application/zip': '.zip', 'application/x-zip-compressed': '.zip', 'application/gzip': '.gz', 'application/x-tar': '.tar', 'application/vnd.rar': '.rar', 'application/x-7z-compressed': '.7z', 'application/msword': '.doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', 'application/vnd.ms-excel': '.xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', 'application/vnd.ms-powerpoint': '.ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', 'application/vnd.oasis.opendocument.text': '.odt', 'application/vnd.oasis.opendocument.spreadsheet': '.ods', 'application/vnd.oasis.opendocument.presentation': '.odp', 'audio/mpeg': '.mp3', 'audio/mp4': '.m4a', 'audio/wav': '.wav', 'audio/ogg': '.ogg', 'video/mp4': '.mp4', 'video/webm': '.webm', 'video/x-matroska': '.mkv', 'video/quicktime': '.mov', } const TYPES = { file: { name: 'Google Drive File', formats: [], defaultFormat: null, url: (id) => `https://drive.google.com/uc?export=download&confirm=t&id=${id}`, }, document: { name: 'Google Document', formats: ['pdf', 'docx', 'odt', 'rtf', 'txt', 'html'], defaultFormat: 'pdf', url: (id, fmt) => `https://docs.google.com/document/d/${id}/export?format=${fmt}`, }, spreadsheets: { name: 'Google Spreadsheet', formats: ['xlsx', 'pdf', 'ods', 'csv', 'tsv'], defaultFormat: 'xlsx', url: (id, fmt) => `https://docs.google.com/spreadsheets/d/${id}/export?format=${fmt}`, }, presentation: { name: 'Google Presentation', formats: ['pptx', 'pdf', 'odp', 'txt'], defaultFormat: 'pptx', url: (id, fmt) => `https://docs.google.com/presentation/d/${id}/export/${fmt}`, }, drawing: { name: 'Google Drawing', formats: ['png', 'jpeg', 'svg', 'pdf'], defaultFormat: 'png', url: (id, fmt) => `https://docs.google.com/drawings/d/${id}/export/${fmt}`, }, folder: { name: 'Google Drive Folder', formats: [], defaultFormat: null, url: null, }, } const URL_PATTERNS = [ { regex: /drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/, type: 'file' }, { regex: /drive\.google\.com\/open\?id=([a-zA-Z0-9_-]+)/, type: 'file' }, { regex: /drive\.google\.com\/uc\?(?:.*&)?id=([a-zA-Z0-9_-]+)/, type: 'file' }, { regex: /docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/, type: 'document' }, { regex: /docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9_-]+)/, type: 'spreadsheets' }, { regex: /docs\.google\.com\/presentation\/d\/([a-zA-Z0-9_-]+)/, type: 'presentation' }, { regex: /docs\.google\.com\/drawings\/d\/([a-zA-Z0-9_-]+)/, type: 'drawing' }, { regex: /drive\.google\.com\/drive\/folders\/([a-zA-Z0-9_-]+)/, type: 'folder' }, { regex: /[?&]id=([a-zA-Z0-9_-]+)/, type: 'file' }, ] /** * Parse input (URL atau file ID polos) menjadi { fileId, type }. * @returns {{fileId:string, type:string}|null} */ function parseGoogleUrl(input) { const value = String(input || '').trim() if (!value) return null // File ID polos (hanya alfanumerik, strip, underscore) dianggap file Drive. if (/^[a-zA-Z0-9_-]{6,}$/.test(value) && !value.includes('.')) { return { fileId: value, type: 'file' } } for (const pattern of URL_PATTERNS) { const match = value.match(pattern.regex) if (match && match[1]) { return { fileId: match[1], type: pattern.type } } } return null } /** * Generate link download langsung. * @returns {{fileId:string, type:string, typeName:string, formats:string[], defaultFormat:string|null, format:string|null, downloadUrl:string|null}} */ function generateDownloadLink(input, { format } = {}) { const parsed = parseGoogleUrl(input) if (!parsed) throw new Error('Invalid Google Drive/Docs URL. Example: https://drive.google.com/file/d//view') const config = TYPES[parsed.type] if (parsed.type === 'folder') { throw new Error('Folders cannot be downloaded directly. Share individual files instead.') } let fmt = format || config.defaultFormat || null if (fmt && config.formats.length && !config.formats.includes(fmt)) { throw new Error(`Invalid format "${fmt}". Available: ${config.formats.join(', ')}`) } return { fileId: parsed.fileId, type: parsed.type, typeName: config.name, formats: config.formats, defaultFormat: config.defaultFormat, format: fmt, downloadUrl: config.url(parsed.fileId, fmt), } } /** * Verifikasi link (status + content-type + ukuran). Body dibatalkan setelah header. */ async function checkLink(url) { const res = await fetch(url, { method: 'GET', redirect: 'follow', headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(25000), }) const len = res.headers.get('content-length') if (res.body?.cancel) await res.body.cancel().catch(() => {}) return { status: res.status, ok: res.ok, contentType: res.headers.get('content-type') || '', size: len ? Number(len) : null, finalUrl: res.url || url, } } // Ambil link download asli dari halaman konfirmasi virus-scan Drive (file besar). function extractConfirmLink(html) { const form = /]*action="([^"]+)"[^>]*>/.exec(html) if (form && /drive\.(usercontent\.)?google\.com/.test(form[1])) { return form[1].replace(/&/g, '&') } const anchor = /href="(https:\/\/drive\.usercontent\.google\.com\/download[^"]+)"/.exec(html) if (anchor) return anchor[1].replace(/&/g, '&') return null } // Nama file dari content-disposition, fallback ke ekstensi MIME / format. function resolveFileName(res, result) { const cd = res.headers.get('content-disposition') || '' const star = /filename\*=UTF-8''([^;]+)/i.exec(cd) if (star) { const name = decodeURIComponent(star[1].trim()) if (name) return name } const plain = /filename="?([^";]+)"?/i.exec(cd) if (plain && plain[1]) return plain[1] const mime = (res.headers.get('content-type') || '').split(';')[0].trim().toLowerCase() const ext = MIME_EXT[mime] || (result.format ? `.${result.format}` : '') return `drive_${result.fileId}${ext}` } function resolveOutputPath(outputPath, fileName) { if (!outputPath) return path.join(process.cwd(), fileName) let isDir = false try { isDir = statSync(outputPath).isDirectory() } catch { /* bukan direktori / belum ada */ } if (isDir || outputPath.endsWith('/') || outputPath.endsWith(path.sep)) { return path.join(outputPath, fileName) } return outputPath } /** * Download file ke disk. Streaming (tanpa buffer penuh di memori). * @returns {Promise<{filePath:string, fileName:string, size:number, contentType:string, url:string, type:string}>} */ async function downloadFile(input, { format, outputPath, timeout = 120000 } = {}) { const result = generateDownloadLink(input, { format }) let res = await fetch(result.downloadUrl, { redirect: 'follow', headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(timeout), }) if (!res.ok) { await res.body?.cancel?.().catch(() => {}) throw new Error(`HTTP ${res.status}`) } let finalUrl = res.url || result.downloadUrl // File Drive besar: uc mengembalikan halaman konfirmasi virus (text/html). // File non-publik: uc mengembalikan halaman Sign-in. const ctype = (res.headers.get('content-type') || '').toLowerCase() if (ctype.includes('text/html')) { const html = await res.text() if (/sign-in|servicelogin|\/v3\/signin\//i.test(html)) { throw new Error('File tidak publik (butuh login Google). Pastikan file di-share sebagai Anyone with the link.') } const direct = extractConfirmLink(html) if (!direct) { throw new Error('Drive meminta konfirmasi tetapi link download tidak ditemukan.') } res = await fetch(direct, { redirect: 'follow', headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(timeout), }) if (!res.ok) { await res.body?.cancel?.().catch(() => {}) throw new Error(`HTTP ${res.status}`) } finalUrl = res.url || direct } const fileName = resolveFileName(res, result) const filePath = resolveOutputPath(outputPath, fileName) await fs.mkdir(path.dirname(filePath), { recursive: true }) const nodeStream = Readable.fromWeb(res.body) await pipeline(nodeStream, createWriteStream(filePath)) const stat = await fs.stat(filePath) return { filePath, fileName, size: stat.size, contentType: res.headers.get('content-type') || '', url: finalUrl, type: result.type, } } // ===== SELF-TEST: node gdrive.js [--format ] [--check] [--download ] ===== const isDirectRun = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href if (isDirectRun) { ;(async () => { const args = process.argv.slice(2) const input = args.find((a) => !a.startsWith('--')) const fmtArg = args.indexOf('--format') >= 0 ? args[args.indexOf('--format') + 1] : undefined const doCheck = args.includes('--check') const dlIdx = args.indexOf('--download') const dlPath = dlIdx >= 0 ? (args[dlIdx + 1] && !args[dlIdx + 1].startsWith('--') ? args[dlIdx + 1] : undefined) : undefined const doDownload = dlIdx >= 0 if (!input) { console.log('Usage: node gdrive.js [--format ] [--check] [--download ]') console.log('') console.log('Examples:') console.log(' node gdrive.js https://drive.google.com/file/d//view') console.log(' node gdrive.js --check') console.log(' node gdrive.js https://docs.google.com/document/d//edit --format pdf') console.log(' node gdrive.js --download ./downloads') console.log(' node gdrive.js --format mp4') process.exit(0) } try { const result = generateDownloadLink(input, { format: fmtArg }) console.log('== Google Drive downloader ==') console.log(`input : ${input}`) console.log(`type : ${result.type} (${result.typeName})`) console.log(`id : ${result.fileId}`) if (result.formats.length) { console.log(`formats: ${result.formats.join(', ')}`) console.log(`format: ${result.format} (default ${result.defaultFormat})`) } console.log(`url : ${result.downloadUrl}`) if (doCheck) { const info = await checkLink(result.downloadUrl) 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'}`) } if (doDownload) { const dl = await downloadFile(input, { format: fmtArg, outputPath: dlPath }) console.log(`saved : ${dl.filePath}`) console.log(`name : ${dl.fileName}`) console.log(`size : ${(dl.size / 1048576).toFixed(2)} MB (${dl.size} bytes)`) console.log(`type : ${dl.contentType || 'unknown'}`) } console.log('done') } catch (e) { console.error(`ERROR: ${e.message}`) process.exit(1) } })() } export { generateDownloadLink, parseGoogleUrl, checkLink, downloadFile }; export default generateDownloadLink; /* Legacy generic self-test intentionally removed; the CLI above is the single entry point. */