├── LEARN.md ├── CONTRIBUTING.md ├── .eslintrc.json ├── next.config.js ├── public └── favicon.ico ├── pages ├── _app.tsx ├── _document.tsx ├── api │ └── hello.ts └── index.tsx ├── .gitignore ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── nextjs.yml ├── package.json ├── LICENSE ├── styles └── globals.scss └── README.md /LEARN.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sauravhathi/github-repository-downloader/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.scss' 2 | import type { AppProps } from 'next/app' 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return 6 | } 7 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["./*"] 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-repository-downloader", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@types/node": "18.14.2", 13 | "@types/react": "18.0.28", 14 | "@types/react-dom": "18.0.11", 15 | "axios": "^1.3.4", 16 | "eslint": "8.35.0", 17 | "eslint-config-next": "13.2.1", 18 | "jszip": "^3.10.1", 19 | "lru-cache": "^7.18.3", 20 | "memoizee": "^0.4.15", 21 | "next": "13.2.1", 22 | "p-map": "^5.5.0", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "sass": "^1.58.3", 26 | "save-file": "^2.3.1", 27 | "typescript": "4.9.5" 28 | }, 29 | "devDependencies": { 30 | "@types/file-saver": "^2.0.5", 31 | "@types/memoizee": "^0.4.8" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Saurav Hathi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body{ 8 | background: #f5f5f5; 9 | font-family: 'Roboto', sans-serif; 10 | } 11 | 12 | .App{ 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | height: 100vh; 17 | background: #f5f5f5; 18 | 19 | main{ 20 | width: 100%; 21 | max-width: 800px; 22 | background: #fff; 23 | padding: 20px; 24 | border-radius: 5px; 25 | box-shadow: 0 0 10px rgba(0,0,0,0.1); 26 | 27 | h1{ 28 | font-weight: 500; 29 | margin-bottom: 5px; 30 | text-align: center; 31 | } 32 | 33 | p{ 34 | color: #333; 35 | margin-bottom: 2rem; 36 | text-align: center; 37 | } 38 | 39 | .press-enter{ 40 | font-size: 14px; 41 | color: #333; 42 | margin-bottom: 5px; 43 | } 44 | 45 | .input-box{ 46 | display: flex; 47 | flex-direction: column; 48 | gap: 10px; 49 | margin-bottom: 20px; 50 | 51 | input{ 52 | width: 100%; 53 | padding: 10px; 54 | border: 1px solid #ccc; 55 | border-radius: 5px; 56 | outline: none; 57 | } 58 | 59 | button{ 60 | padding: 10px 20px; 61 | border: none; 62 | border-radius: 5px; 63 | background: #333; 64 | color: #fff; 65 | cursor: pointer; 66 | outline: none; 67 | align-self: flex-end; 68 | text-align: center; 69 | } 70 | } 71 | 72 | .output{ 73 | margin-bottom: 20px; 74 | 75 | .log{ 76 | background: #f5f5f5; 77 | padding: 10px; 78 | border-radius: 5px; 79 | font-size: 14px; 80 | line-height: 1.5; 81 | white-space: pre-wrap; 82 | } 83 | 84 | .files{ 85 | max-height: 300px; 86 | overflow-y: auto; 87 | margin-top: 10px; 88 | 89 | li{ 90 | padding: 10px; 91 | border-bottom: 1px solid #f5f5f5; 92 | font-size: 14px; 93 | line-height: 1.5; 94 | } 95 | } 96 | 97 | } 98 | 99 | footer{ 100 | text-align: center; 101 | padding: 10px 0; 102 | font-size: 18px; 103 | color: #333; 104 | 105 | a{ 106 | color: #333; 107 | text-decoration: none; 108 | } 109 | } 110 | } 111 | 112 | @media (max-width: 800px){ 113 | main{ 114 | padding: 20px; 115 | } 116 | } 117 | 118 | @media (max-width: 600px){ 119 | main{ 120 | padding: 10px; 121 | } 122 | } 123 | 124 | @media (max-width: 400px){ 125 | main{ 126 | padding: 5px; 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitHub Repository Downloader 2 | [![Deploy Next.js site to Pages](https://github.com/sauravhathi/github-repository-downloader/actions/workflows/nextjs.yml/badge.svg)](https://github.com/sauravhathi/github-repository-downloader/actions/workflows/nextjs.yml) 3 | 4 | GitHub Repository Downloader is a convenient and user-friendly tool that allows you to easily download entire repositories or specific folders from GitHub as a ZIP file. Whether you're a developer looking to download code for offline use, or simply want to access files that are no longer available online, GitHub Repository Downloader makes it easy to get the files you need. 5 | 6 | ### GitHub Folder Tree 🌲 7 | 8 | https://github.com/sauravhathi/github-folder-tree 9 | 10 | image 11 | 12 | ### Demo 13 | ### [https://sauravhathi.github.io/github-repository-downloader/](https://sauravhathi.github.io/github-repository-downloader/) 14 | 15 | ### Screenshots 16 | ![image](https://user-images.githubusercontent.com/61316762/221985520-12450460-d97d-4a32-97de-7055d3dbd6e5.png) 17 | 18 | ![image](https://user-images.githubusercontent.com/61316762/221985559-e38bb434-db8a-473f-9a7d-1c42e9f25ccb.png) 19 | 20 | ### Getting Started 21 | 22 | 1. Clone the repository and install dependencies: 23 | 24 | ```bash 25 | git clone https://github.com/sauravhathi/github-repository-downloader 26 | cd repo 27 | npm install 28 | ``` 29 | 2. Start the development server: 30 | 31 | ```bash 32 | npm run dev 33 | ``` 34 | 3. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 35 | 36 | ### Usage 37 | 1. Enter the URL of the GitHub repository or folder you want to download in the input field. 38 | 2. Press Enter or click the "Download" button to start the download. 39 | 3. Wait for the files to be downloaded and zipped. The progress will be displayed in the log area. 40 | 4. Once the download is complete, a ZIP file containing the contents of the repository or folder will be downloaded to your computer. 41 | 42 | ### Features 43 | - Download entire repositories or specific folders 44 | - User-friendly interface 45 | - Progress tracking 46 | - ZIP file download 47 | 48 | ### Built With 49 | - [Next.js](https://nextjs.org/) 50 | - [React](https://reactjs.org/) 51 | - [SCSS](https://sass-lang.com/) 52 | - [GitHub API](https://docs.github.com/en/rest) 53 | - [JSZip](https://stuk.github.io/jszip/) 54 | - [FileSaver.js](https://github.com/eligrey/FileSaver.js/) 55 | 56 | ### License 57 | This project is licensed under the MIT License - see the [LICENSE](https://github.com/sauravhathi/github-repository-downloader/blob/main/LICENSE) file for details. 58 | -------------------------------------------------------------------------------- /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["main"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow one concurrent deployment 22 | concurrency: 23 | group: "pages" 24 | cancel-in-progress: true 25 | 26 | jobs: 27 | # Build job 28 | build: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Detect package manager 34 | id: detect-package-manager 35 | run: | 36 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 37 | echo "manager=yarn" >> $GITHUB_OUTPUT 38 | echo "command=install" >> $GITHUB_OUTPUT 39 | echo "runner=yarn" >> $GITHUB_OUTPUT 40 | exit 0 41 | elif [ -f "${{ github.workspace }}/package.json" ]; then 42 | echo "manager=npm" >> $GITHUB_OUTPUT 43 | echo "command=ci" >> $GITHUB_OUTPUT 44 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 45 | exit 0 46 | else 47 | echo "Unable to determine packager manager" 48 | exit 1 49 | fi 50 | - name: Setup Node 51 | uses: actions/setup-node@v3 52 | with: 53 | node-version: "16" 54 | cache: ${{ steps.detect-package-manager.outputs.manager }} 55 | - name: Setup Pages 56 | uses: actions/configure-pages@v3 57 | with: 58 | # Automatically inject basePath in your Next.js configuration file and disable 59 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 60 | # 61 | # You may remove this line if you want to manage the configuration yourself. 62 | static_site_generator: next 63 | - name: Restore cache 64 | uses: actions/cache@v3 65 | with: 66 | path: | 67 | .next/cache 68 | # Generate a new cache whenever packages or source files change. 69 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 70 | # If source files changed but packages didn't, rebuild from a prior cache. 71 | restore-keys: | 72 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 73 | - name: Install dependencies 74 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 75 | - name: Build with Next.js 76 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 77 | - name: Static HTML export with Next.js 78 | run: ${{ steps.detect-package-manager.outputs.runner }} next export 79 | - name: Upload artifact 80 | uses: actions/upload-pages-artifact@v1 81 | with: 82 | path: ./out 83 | 84 | # Deployment job 85 | deploy: 86 | environment: 87 | name: github-pages 88 | url: ${{ steps.deployment.outputs.page_url }} 89 | runs-on: ubuntu-latest 90 | needs: build 91 | steps: 92 | - name: Deploy to GitHub Pages 93 | id: deployment 94 | uses: actions/deploy-pages@v1 95 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import React, { useState, useEffect, useRef } from 'react'; 3 | import JSZip from 'jszip'; 4 | import FileSaver from 'file-saver'; 5 | import axios from 'axios'; 6 | 7 | function App() { 8 | 9 | const [repoUrl, setRepoUrl] = useState(''); 10 | const [repoFiles, setRepoFiles] = useState([]); 11 | const [log, setLog] = useState(''); 12 | const [percentCompleted, setPercentCompleted] = useState(0); 13 | const inputRef = useRef(null); 14 | const fileListRef = useRef(null); 15 | 16 | // Fetch folder data 17 | const fetchFolderData = async (folderUrl: string) => { 18 | // get folder data 19 | const response = await axios.get(folderUrl); 20 | return response.data; 21 | }; 22 | 23 | // Download file content 24 | const downloadFileContent = async (file: { download_url: string }, fileName: string, totalSize: number) => { 25 | // get file content as blob 26 | const response = await axios.get(file.download_url, { 27 | responseType: "blob", 28 | onDownloadProgress: (progressEvent) => { 29 | const percentCompleted = Math.round((progressEvent.loaded * 100) / totalSize); 30 | setLog(`Downloading ${fileName}`); 31 | setPercentCompleted(percentCompleted); 32 | } 33 | }); 34 | return response.data; 35 | }; 36 | 37 | // Download folder contents 38 | const downloadFolderContents = async (folder: any, zip: JSZip) => { 39 | for (const item of folder) { 40 | if (item.type === "file") { 41 | // for file, download content and add to zip 42 | setLog(`Queuing download of ${item.name}`); 43 | const fileContent = await downloadFileContent(item, item.name, item.size); 44 | setRepoFiles((prev) => [...prev, item]); 45 | zip.file(item.name, fileContent); 46 | } else if (item.type === "dir") { 47 | // for folder, fetch folder contents and call downloadFolderContents recursively 48 | const subFolder = await fetchFolderData(item.url); 49 | const subFolderZip = zip.folder(item.name); 50 | await downloadFolderContents(subFolder, subFolderZip as JSZip); 51 | } 52 | } 53 | }; 54 | 55 | // Fetch repo contents 56 | const downloadRepositoryContents = async (folderUrl: string) => { 57 | try { 58 | const folderData = await fetchFolderData(folderUrl); 59 | const zip = new JSZip(); 60 | await downloadFolderContents(folderData, zip); 61 | return zip; 62 | } catch (error) { 63 | console.error(`Error: ${error}`); 64 | } 65 | }; 66 | 67 | // Download repo contents 68 | const downloadFolder = async () => { 69 | setLog(''); 70 | setRepoFiles([]); 71 | 72 | // check if url is valid 73 | if (!repoUrl.includes('github.com')) { 74 | setLog('Invalid URL'); 75 | return; 76 | } 77 | 78 | // extract user, repo, and dir from url 79 | const user = repoUrl.split('/')[3]; 80 | const repo = repoUrl.split('/')[4]; 81 | const dir = repoUrl.split('/').slice(7).join('/'); 82 | console.log(user, repo, dir); 83 | 84 | setLog('Fetching repo contents...'); 85 | const zip = await downloadRepositoryContents(`https://api.github.com/repos/${user}/${repo}/contents/${dir}`); 86 | 87 | if (!zip) { 88 | setLog('Rate limit exceeded. Try again in a few minutes.'); 89 | return; 90 | } 91 | 92 | setLog('Zipping...'); 93 | // generate zip file 94 | const content = await zip.generateAsync({ type: 'blob' }); 95 | // download zip file 96 | FileSaver.saveAs(content, `${dir ? dir.replace(/\/|%20/g, '-') : repo}.zip`); 97 | 98 | // get list of files so we can show length 99 | const fileList = zip.file(/.*/); 100 | 101 | setPercentCompleted(0); 102 | setLog(`Downloaded Total ${fileList.length} files\nUser: ${user}\nRepository: ${repoUrl}\nFolder: ${dir}\nSize: ${(content.size / 1024 / 1024).toFixed(2)} MB`); 103 | }; 104 | 105 | // Scroll to bottom of file list 106 | useEffect(() => { 107 | if (fileListRef.current) { 108 | fileListRef.current.scrollTop = fileListRef.current.scrollHeight; 109 | } 110 | }, [repoFiles]); 111 | 112 | // Focus on input field 113 | useEffect(() => { 114 | if (inputRef.current) { 115 | inputRef.current.focus(); 116 | } 117 | }, []); 118 | 119 | return ( 120 | <> 121 | 122 | {/* 123 | 124 | 125 | 126 | 127 | Saurav Hathi 128 | 129 | 130 | 131 | 132 | 133 | */} 134 | GitHub Repository Downloader 135 | {/* description */} 136 | 138 | {/* keywords */} 139 | 141 | 142 | 143 | 144 |
145 |
146 |

GitHub Repository Downloader

147 |

Download GitHub repositories and folders with ease using GitHub Repository Downloader.

148 |
149 | Press Enter to start download 150 | setRepoUrl(e.target.value)} 154 | placeholder="Enter GitHub repo URL" 155 | onKeyDown={(e) => { 156 | if (e.key === 'Enter') { 157 | downloadFolder(); 158 | } 159 | }} /> 160 | 165 |
166 | 167 | {log && ( 168 |
169 |
 0 ? `linear-gradient(to right, #f0f0f0 ${percentCompleted}%, #fff ${percentCompleted}%)` : undefined,
174 |                 }}
175 |               >
176 |                 {log}{" "}
177 |                 {percentCompleted > 0 && (
178 |                   {percentCompleted}% Completed
179 |                 )}
180 |               
181 |
182 | )} 183 | 184 | {repoFiles.length > 0 &&
185 |
    188 | {repoFiles.map((file, index) => ( 189 |
  • {file.name}
  • 190 | ))} 191 |
192 |
} 193 | 194 |

195 | GitHub Repository Downloader is currently not compatible with Google Chrome. We apologize for any inconvenience this may cause. In the meantime, we recommend using another browser such as Firefox or Edge to access our tool and download repositories or folders from GitHub. 196 |

197 | 202 |
203 |
204 | 205 | ); 206 | } 207 | 208 | export default App; --------------------------------------------------------------------------------