├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── codeql.yaml ├── .gitignore ├── LICENSE ├── README.md ├── fs.go ├── go.mod ├── go.sum ├── main.go ├── public ├── favicon.ico ├── font.woff ├── index.html ├── robots.txt ├── script.js └── style.css └── walk ├── dirent.go ├── getdents_stdlib.go ├── getdents_unix.go ├── walk.go └── walk_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | test=auto 2 | 3 | # gofmt enforces LF line endings 4 | *.go text eol=lf 5 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | name: ${{ matrix.os }}, go${{ matrix.go }} 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | go: [1.17] 14 | 15 | defaults: 16 | run: 17 | shell: bash 18 | 19 | steps: 20 | - name: Checkout repository and submodules 21 | uses: actions/checkout@v2 22 | 23 | - name: Set up Go ${{ matrix.go }} 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: ${{ matrix.go }} 27 | 28 | - name: Vet 29 | run: go vet ./... 30 | 31 | - name: Test 32 | run: go test ./... 33 | 34 | - name: Build 35 | run: go build 36 | 37 | - name: Prepare artifacts 38 | run: | 39 | REF_NAME=$(echo "${GITHUB_HEAD_REF:-$GITHUB_REF_NAME}" | sed 's/[^a-zA-Z0-9\-_.]/-/g') 40 | echo "ref_name=$REF_NAME" >> $GITHUB_ENV 41 | echo -e "Author: Niels A.D. 42 | Project: autoindex (https://github.com/nielsAD/autoindex) 43 | Platform: `go env GOOS`/`go env GOARCH` (`go env GOVERSION`) 44 | Release: $REF_NAME ($GITHUB_SHA) 45 | Date: `date -u`" > VERSION.txt 46 | mv autoindex.exe autoindex-$REF_NAME.exe || true 47 | mv autoindex autoindex-$REF_NAME || true 48 | mv LICENSE LICENSE.txt 49 | 50 | - name: Upload artifacts 51 | uses: actions/upload-artifact@v2 52 | with: 53 | name: autoindex_${{ env.ref_name }}_${{ runner.os }} 54 | path: | 55 | public/ 56 | autoindex-* 57 | LICENSE.txt 58 | VERSION.txt 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yaml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | schedule: 8 | - cron: '0 0 * * 0' 9 | 10 | jobs: 11 | analyze: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | security-events: write 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - name: Initialize CodeQL 21 | uses: github/codeql-action/init@v1 22 | with: 23 | languages: go 24 | 25 | - name: Perform CodeQL Analysis 26 | uses: github/codeql-action/analyze@v1 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test binary (go test -c) 2 | *.test 3 | 4 | # Output of the go coverage tool 5 | *.out 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | autoindex 2 | ========= 3 | [![Build Status](https://github.com/nielsAD/autoindex/actions/workflows/build.yml/badge.svg)](https://github.com/nielsAD/autoindex/actions) 4 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) 5 | 6 | Lightweight `go` web server that provides a searchable directory index. Optimized for handling large numbers of files (100k+) and remote file systems (with high latency) through a continously updated directory cache. 7 | 8 | [Live demo](https://archive.toom.io/) 9 | 10 | #### Features: 11 | 12 | * Lightweight single-page application (`~8KB html/css/js`) 13 | * Responsive design 14 | * Recursive file search 15 | * Directory cache (`sqlite`) 16 | * Sitemap support 17 | 18 | 19 | Usage 20 | ----- 21 | 22 | `./autoindex [options]` 23 | 24 | | Flag | Type | Description | 25 | |------------|----------|-------------| 26 | |`-a` |`string` |TCP network address to listen for connections| 27 | |`-d` |`string` |Database location| 28 | |`-r` |`string` |Root directory to serve| 29 | |`-i` |`string` |Refresh interval| 30 | |`-l` |`int` |Request rate limit (req/sec per IP)| 31 | |`-t` |`duration`|Request timeout| 32 | |`-forwarded`|`bool` |Trust X-Real-IP and X-Forwarded-For headers| 33 | |`-cached` |`bool` |Serve everything from cache (rather than search/recursive queries only)| 34 | 35 | #### Example 36 | 37 | `./autoindex -a=":4000" -i=5m -d=/tmp/autoindex.db -cached -r=/mnt/storage` 38 | 39 | 40 | Behind nginx 41 | ------------ 42 | 43 | Example configuration for running `autoindex` behind an `nginx` proxy. 44 | 45 | ``` 46 | upstream autoindex { 47 | server 127.0.0.1:4000; 48 | keepalive 8; 49 | } 50 | 51 | map $request_uri $request_basename { 52 | ~/(?[^/?]*)(?:\?|$) $captured_request_basename; 53 | } 54 | 55 | map $request_uri $idx_path { 56 | ~/(?\?.*)?$ $captured_request_args; 57 | ~/(?[^?]*)(?\?.*)?$ $captured_request_path/$captured_request_args; 58 | } 59 | 60 | server { 61 | listen 443 ssl http2; 62 | listen [::]:443 ssl http2; 63 | server_name _; 64 | 65 | root /opt/autoindex/public; 66 | 67 | location / { 68 | rewrite ^/(.*)/$ /$1 permanent; 69 | try_files $uri /index.html; 70 | expires 1y; 71 | } 72 | 73 | location = /index.html { 74 | http2_push /idx/$idx_path; 75 | expires 1d; 76 | } 77 | 78 | location ^~ /dl/ { 79 | limit_rate 1m; 80 | add_header Content-Disposition 'attachment; filename="$request_basename"'; 81 | add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive"; 82 | } 83 | 84 | location ~ ^(/idx/|/urllist.txt) { 85 | proxy_pass http://autoindex; 86 | } 87 | } 88 | ``` 89 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | "encoding/json" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "path" 15 | "path/filepath" 16 | "regexp" 17 | "sort" 18 | "strings" 19 | "sync/atomic" 20 | "time" 21 | 22 | _ "github.com/mattn/go-sqlite3" 23 | "github.com/nielsAD/autoindex/walk" 24 | ) 25 | 26 | // CachedFS struct 27 | type CachedFS struct { 28 | ql *sql.Stmt 29 | qd *sql.Stmt 30 | qs *sql.Stmt 31 | db *sql.DB 32 | dbr int32 33 | dbp string 34 | Root string 35 | Cached bool 36 | Timeout time.Duration 37 | } 38 | 39 | // New CachedFS 40 | func New(dbp string, root string) (*CachedFS, error) { 41 | r, err := filepath.Abs(root) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | db, err := sql.Open("sqlite3", dbp) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if _, err := db.Exec(` 52 | CREATE TABLE IF NOT EXISTS dirs (path TEXT); 53 | CREATE TABLE IF NOT EXISTS files (root INTEGER, name TEXT, dir BOOLEAN) 54 | `); err != nil { 55 | db.Close() 56 | return nil, err 57 | } 58 | 59 | ql, err := db.Prepare("SELECT dirs.path FROM dirs LIMIT 50000") 60 | if err != nil { 61 | db.Close() 62 | return nil, err 63 | } 64 | 65 | qd, err := db.Prepare("SELECT dirs.rowid FROM dirs WHERE path GLOB ? LIMIT 1") 66 | if err != nil { 67 | db.Close() 68 | return nil, err 69 | } 70 | 71 | qs, err := db.Prepare("SELECT dirs.path, files.name, files.dir FROM files LEFT JOIN dirs ON files.root = dirs.rowid WHERE files.root IN (SELECT rowid FROM dirs WHERE path GLOB ?) AND files.name LIKE ? ESCAPE '`' LIMIT 1000") 72 | if err != nil { 73 | db.Close() 74 | return nil, err 75 | } 76 | 77 | fs := CachedFS{ 78 | ql: ql, 79 | qd: qd, 80 | qs: qs, 81 | db: db, 82 | dbp: dbp, 83 | Root: r, 84 | } 85 | 86 | // Check if database already has root entry 87 | var id int64 88 | if fs.qd.QueryRow("/").Scan(&id) == nil { 89 | fs.dbr++ 90 | } 91 | 92 | return &fs, nil 93 | } 94 | 95 | // Close closes the database, releasing any open resources. 96 | func (fs *CachedFS) Close() error { 97 | return fs.db.Close() 98 | } 99 | 100 | const ( 101 | insDir = "INSERT INTO dirs_tmp (path) VALUES (?)" 102 | insFile = "INSERT INTO files_tmp (root, name, dir) VALUES (?, ?, ?)" 103 | ) 104 | 105 | // Fill database 106 | func (fs *CachedFS) Fill() (int, error) { 107 | if _, err := fs.db.Exec(` 108 | DROP TABLE IF EXISTS dirs_tmp; 109 | DROP TABLE IF EXISTS files_tmp; 110 | CREATE TABLE dirs_tmp (path TEXT); 111 | CREATE TABLE files_tmp (root INTEGER, name TEXT, dir BOOLEAN) 112 | `); err != nil { 113 | return 0, err 114 | } 115 | 116 | tx, err := fs.db.Begin() 117 | if err != nil { 118 | return 0, err 119 | } 120 | idir, err := tx.Prepare(insDir) 121 | if err != nil { 122 | return 0, err 123 | } 124 | ifile, err := tx.Prepare(insFile) 125 | if err != nil { 126 | return 0, err 127 | } 128 | 129 | cnt := 0 130 | dirs := []int64{0} 131 | root := fs.Root 132 | trim := len(fs.Root) 133 | 134 | if strings.HasSuffix(root, string(filepath.Separator)) { 135 | trim-- 136 | } else { 137 | root += string(filepath.Separator) 138 | } 139 | 140 | err = walk.Walk(fs.Root, &walk.Options{ 141 | Error: func(r string, e *walk.Dirent, err error) error { 142 | logErr.Printf("Error iterating \"%s\": %s\n", r, err.Error()) 143 | return nil 144 | }, 145 | Visit: func(r string, e *walk.Dirent) error { 146 | // Skip root 147 | if cnt == 0 { 148 | cnt++ 149 | return nil 150 | } 151 | 152 | n := e.Name() 153 | if n == "" || strings.HasPrefix(n, ".") { 154 | return nil 155 | } 156 | 157 | if _, err := ifile.Exec(dirs[len(dirs)-1], n, e.IsDir()); err != nil { 158 | return err 159 | } 160 | 161 | cnt++ 162 | if cnt%16384 == 0 { 163 | if err := tx.Commit(); err != nil { 164 | return err 165 | } 166 | tx, err = fs.db.Begin() 167 | if err != nil { 168 | return err 169 | } 170 | idir, err = tx.Prepare(insDir) 171 | if err != nil { 172 | return err 173 | } 174 | ifile, err = tx.Prepare(insFile) 175 | if err != nil { 176 | return err 177 | } 178 | } 179 | 180 | return nil 181 | }, 182 | Enter: func(r string, e *walk.Dirent) error { 183 | if strings.HasPrefix(e.Name(), ".") { 184 | return filepath.SkipDir 185 | } 186 | 187 | if e.IsSymlink() { 188 | e, err := filepath.EvalSymlinks(r) 189 | if err != nil { 190 | return err 191 | } 192 | a, err := filepath.Abs(e) 193 | if err != nil { 194 | return err 195 | } 196 | if strings.HasPrefix(a, root) { 197 | logErr.Printf("Skipping symlink relative to root (%s)\n", r) 198 | return filepath.SkipDir 199 | } 200 | } 201 | 202 | dir := filepath.ToSlash(r[trim:]) 203 | if dir != "/" { 204 | dir += "/" 205 | } 206 | 207 | row, err := idir.Exec(dir) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | id, err := row.LastInsertId() 213 | if err != nil { 214 | return err 215 | } 216 | 217 | dirs = append(dirs, id) 218 | return nil 219 | }, 220 | Leave: func(r string, e *walk.Dirent, err error) error { 221 | dirs = dirs[:len(dirs)-1] 222 | return err 223 | }, 224 | }) 225 | if err != nil { 226 | tx.Rollback() 227 | return 0, err 228 | } 229 | 230 | if _, err := tx.Exec(` 231 | DROP TABLE IF EXISTS dirs; 232 | DROP TABLE IF EXISTS files; 233 | ALTER TABLE dirs_tmp RENAME TO dirs; 234 | ALTER TABLE files_tmp RENAME TO files; 235 | CREATE INDEX idx_dirs ON dirs (path); 236 | CREATE INDEX idx_files ON files (root); 237 | `); err != nil { 238 | return 0, err 239 | } 240 | 241 | if err := tx.Commit(); err != nil { 242 | return 0, err 243 | } 244 | 245 | fs.db.Exec("VACUUM; PRAGMA shrink_memory") 246 | atomic.AddInt32(&fs.dbr, 1) 247 | 248 | return cnt, nil 249 | } 250 | 251 | // DBReady returns whether the DB is ready for querying 252 | func (fs *CachedFS) DBReady() bool { 253 | return fs.db != nil && atomic.LoadInt32(&fs.dbr) != 0 254 | } 255 | 256 | var ( 257 | escGlob = regexp.MustCompile(`[][*?]`) 258 | escLike = regexp.MustCompile("[%_`]") 259 | escSpace = regexp.MustCompile(`\s+`) 260 | ) 261 | 262 | func escapeGlob(s string) string { 263 | return escGlob.ReplaceAllStringFunc(s, func(m string) string { return "[" + m + "]" }) 264 | } 265 | 266 | func escapeLike(s string) string { 267 | s = strings.TrimSpace(s) 268 | if s == "" { 269 | return "%" 270 | } 271 | 272 | s = escLike.ReplaceAllStringFunc(s, func(m string) string { return "`" + m }) 273 | s = escSpace.ReplaceAllString(s, "%") 274 | return "%" + s + "%" 275 | } 276 | 277 | func escapeRegex(s string) string { 278 | s = strings.TrimSpace(s) 279 | if s == "" { 280 | return ".*" 281 | } 282 | s = regexp.QuoteMeta(s) 283 | s = escSpace.ReplaceAllString(s, ".*") 284 | return "(?i).*" + s + ".*" 285 | } 286 | 287 | func cleanPath(p string) string { 288 | if !strings.HasPrefix(p, "/") { 289 | p = "/" + p 290 | } 291 | p = path.Clean(p) 292 | if !strings.HasSuffix(p, "/") { 293 | p += "/" 294 | } 295 | return p 296 | } 297 | 298 | // File data sent to client 299 | type File struct { 300 | Name string `json:"name"` 301 | Type string `json:"type"` 302 | } 303 | 304 | // Files list (sortable) 305 | type Files []File 306 | 307 | func (f Files) Len() int { return len(f) } 308 | func (f Files) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 309 | func (f Files) Less(i, j int) bool { 310 | if f[i].Type == f[j].Type { 311 | return strings.ToLower(f[i].Name) < strings.ToLower(f[j].Name) 312 | } 313 | return f[i].Type < f[j].Type 314 | } 315 | 316 | func (fs *CachedFS) serveCache(w http.ResponseWriter, r *http.Request) { 317 | if !fs.DBReady() { 318 | http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable) 319 | return 320 | } 321 | 322 | ctx, cancel := context.WithTimeout(r.Context(), fs.Timeout) 323 | defer cancel() 324 | 325 | p := cleanPath(r.URL.Path) 326 | trim := len(p) 327 | 328 | p = escapeGlob(p) 329 | if r.URL.Query().Get("r") != "" { 330 | p += "*" 331 | } 332 | 333 | var id int64 334 | if err := fs.qd.QueryRowContext(ctx, p).Scan(&id); err == sql.ErrNoRows { 335 | http.NotFound(w, r) 336 | return 337 | } else if err != nil { 338 | logError(http.StatusInternalServerError, err, w, r) 339 | return 340 | } 341 | 342 | resp := make(Files, 0) 343 | search := escapeLike(r.URL.Query().Get("q")) 344 | 345 | rows, err := fs.qs.QueryContext(ctx, p, search) 346 | if err != nil { 347 | logError(http.StatusInternalServerError, err, w, r) 348 | return 349 | } 350 | defer rows.Close() 351 | 352 | for rows.Next() { 353 | var root string 354 | var name string 355 | var dir bool 356 | if err := rows.Scan(&root, &name, &dir); err != nil { 357 | logError(http.StatusInternalServerError, err, w, r) 358 | return 359 | } 360 | 361 | f := File{Name: root[trim:] + name} 362 | if dir { 363 | f.Type = "d" 364 | } else { 365 | f.Type = "f" 366 | } 367 | 368 | resp = append(resp, f) 369 | } 370 | 371 | if err := rows.Err(); err != nil { 372 | logError(http.StatusInternalServerError, err, w, r) 373 | return 374 | } 375 | 376 | sort.Sort(resp) 377 | 378 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 379 | w.Header().Set("Cache-Control", "max-age=60") 380 | json.NewEncoder(w).Encode(resp) 381 | } 382 | 383 | func (fs *CachedFS) serveLive(w http.ResponseWriter, r *http.Request) { 384 | p := filepath.Join(fs.Root, filepath.FromSlash(r.URL.Path), "_") 385 | p = p[:len(p)-1] 386 | 387 | resp := make(Files, 0) 388 | search, err := regexp.Compile(escapeRegex(r.URL.Query().Get("q"))) 389 | 390 | if err == nil { 391 | trim := len(p) 392 | depth := 0 393 | err = walk.Walk(p, &walk.Options{ 394 | Error: func(r string, e *walk.Dirent, err error) error { 395 | logErr.Printf("Error iterating \"%s\": %s\n", r, err.Error()) 396 | return nil 397 | }, 398 | Visit: func(r string, e *walk.Dirent) error { 399 | if depth == 0 { 400 | return nil 401 | } 402 | 403 | n := e.Name() 404 | if n == "" || strings.HasPrefix(n, ".") || !search.MatchString(n) { 405 | return nil 406 | } 407 | 408 | f := File{Name: filepath.ToSlash(r[trim:])} 409 | if e.IsDir() { 410 | f.Type = "d" 411 | } else { 412 | f.Type = "f" 413 | } 414 | 415 | resp = append(resp, f) 416 | 417 | return nil 418 | }, 419 | Enter: func(r string, e *walk.Dirent) error { 420 | if depth >= 1 { 421 | return filepath.SkipDir 422 | } 423 | depth++ 424 | return nil 425 | }, 426 | Leave: func(r string, e *walk.Dirent, err error) error { 427 | depth-- 428 | return err 429 | }, 430 | }) 431 | } 432 | 433 | if err == walk.ErrNonDir || os.IsNotExist(err) || os.IsPermission(err) { 434 | http.NotFound(w, r) 435 | return 436 | } else if err != nil { 437 | logError(http.StatusInternalServerError, err, w, r) 438 | return 439 | } 440 | 441 | sort.Sort(resp) 442 | 443 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 444 | w.Header().Set("Cache-Control", "max-age=60") 445 | json.NewEncoder(w).Encode(resp) 446 | } 447 | 448 | func (fs *CachedFS) ServeHTTP(w http.ResponseWriter, r *http.Request) { 449 | if fs.Cached || r.URL.Query().Get("r") != "" { 450 | fs.serveCache(w, r) 451 | } else { 452 | fs.serveLive(w, r) 453 | } 454 | } 455 | 456 | // Sitemap serves a list of all directories 457 | func (fs *CachedFS) Sitemap(w http.ResponseWriter, r *http.Request) { 458 | if !fs.DBReady() { 459 | http.Error(w, "503 Service Unavailable", http.StatusServiceUnavailable) 460 | return 461 | } 462 | 463 | ctx, cancel := context.WithTimeout(r.Context(), fs.Timeout) 464 | defer cancel() 465 | 466 | u, err := url.Parse("https://" + r.Host) 467 | if err != nil { 468 | logError(http.StatusInternalServerError, err, w, r) 469 | return 470 | } 471 | 472 | rows, err := fs.ql.QueryContext(ctx) 473 | if err != nil { 474 | logError(http.StatusInternalServerError, err, w, r) 475 | return 476 | } 477 | defer rows.Close() 478 | 479 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 480 | w.Header().Set("Cache-Control", "max-age=3600") 481 | 482 | for rows.Next() { 483 | var path string 484 | if err := rows.Scan(&path); err != nil { 485 | logError(http.StatusInternalServerError, err, w, r) 486 | return 487 | } 488 | 489 | u.Path = path[:len(path)-1] 490 | w.Write([]byte(u.String())) 491 | w.Write([]byte{'\n'}) 492 | } 493 | 494 | if err := rows.Err(); err != nil { 495 | logError(http.StatusInternalServerError, err, w, r) 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nielsAD/autoindex 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/mattn/go-sqlite3 v1.14.13 7 | github.com/ulule/limiter/v3 v3.10.0 8 | ) 9 | 10 | require github.com/pkg/errors v0.9.1 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 9 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 10 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 11 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 12 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 13 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 14 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 15 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 16 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 19 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 20 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 21 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 22 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 23 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 24 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 25 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 26 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 27 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 28 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 29 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 32 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 33 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 34 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 35 | github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 36 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 37 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 38 | github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= 39 | github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 40 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 41 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 42 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 43 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 44 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 45 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 46 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 47 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 48 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 49 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 50 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 51 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 55 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 56 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 57 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 58 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 59 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 61 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 62 | github.com/ulule/limiter/v3 v3.10.0 h1:C9mx3tgxYnt4pUYKWktZf7aEOVPbRYxR+onNFjQTEp0= 63 | github.com/ulule/limiter/v3 v3.10.0/go.mod h1:NqPA/r8QfP7O11iC+95X6gcWJPtRWjKrtOUw07BTvoo= 64 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 65 | github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= 66 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 67 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 68 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 69 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 70 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 71 | golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 72 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 73 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 74 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 75 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 76 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 77 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 78 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 79 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 80 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 81 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 85 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 97 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 99 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 100 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 101 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 102 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 103 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 104 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 105 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 106 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 107 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 108 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 109 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 111 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 112 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 113 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 114 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 115 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 116 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 117 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 118 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 119 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 120 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 121 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 123 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 124 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 125 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 126 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 127 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 128 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 129 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "log" 11 | "net" 12 | "net/http" 13 | "os" 14 | "os/signal" 15 | "path" 16 | "strings" 17 | "syscall" 18 | "time" 19 | 20 | _ "github.com/mattn/go-sqlite3" 21 | "github.com/ulule/limiter/v3" 22 | "github.com/ulule/limiter/v3/drivers/middleware/stdlib" 23 | "github.com/ulule/limiter/v3/drivers/store/memory" 24 | ) 25 | 26 | var ( 27 | addr = flag.String("a", ":80", "TCP network address to listen for connections") 28 | db = flag.String("d", "file::memory:?cache=shared", "Database location") 29 | dir = flag.String("r", ".", "Root directory to serve") 30 | refresh = flag.String("i", "1h", "Refresh interval") 31 | ratelimit = flag.Int64("l", 5, "Request rate limit (req/sec per IP)") 32 | timeout = flag.Duration("t", time.Second, "Request timeout") 33 | forwarded = flag.Bool("forwarded", false, "Trust X-Real-IP and X-Forwarded-For headers") 34 | cached = flag.Bool("cached", false, "Serve everything from cache (rather than search/recursive queries only)") 35 | ) 36 | 37 | var logOut = log.New(os.Stdout, "", 0) 38 | var logErr = log.New(os.Stderr, "", 0) 39 | 40 | func main() { 41 | flag.Parse() 42 | 43 | var interval time.Duration 44 | if *refresh != "" { 45 | i, err := time.ParseDuration(*refresh) 46 | if err != nil { 47 | logErr.Fatal(err) 48 | } 49 | interval = i 50 | } 51 | 52 | fs, err := New(*db, *dir) 53 | if err != nil { 54 | logErr.Fatal(err) 55 | } 56 | 57 | fs.Timeout = *timeout 58 | fs.Cached = *cached 59 | defer fs.Close() 60 | 61 | go func() { 62 | last := 0 63 | for { 64 | n, err := fs.Fill() 65 | if err != nil { 66 | logErr.Printf("Fill: %s\n", err.Error()) 67 | } 68 | if n != last { 69 | logErr.Printf("%d records in database after update (%+d)\n", n, n-last) 70 | last = n 71 | } 72 | if interval == 0 { 73 | break 74 | } 75 | time.Sleep(interval) 76 | } 77 | }() 78 | 79 | pub := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 80 | p := path.Join("./public/", r.URL.Path) 81 | if s, err := os.Stat(p); err == nil && !s.IsDir() { 82 | http.ServeFile(w, r, p) 83 | } else { 84 | http.ServeFile(w, r, "./public/index.html") 85 | } 86 | }) 87 | 88 | limit := stdlib.NewMiddleware(limiter.New(memory.NewStore(), limiter.Rate{Period: time.Second, Limit: *ratelimit})) 89 | 90 | srv := &http.Server{Addr: *addr} 91 | handleDefault := func(p string, h http.Handler) { http.Handle(p, realIP(*forwarded, checkMethod(h))) } 92 | handleLimited := func(p string, h http.Handler) { handleDefault(p, limit.Handler(logRequest(http.StripPrefix(p, h)))) } 93 | 94 | handleLimited("/idx/", fs) 95 | handleLimited("/dl/", nodir(http.FileServer(http.Dir(fs.Root)))) 96 | handleLimited("/urllist.txt", http.HandlerFunc(fs.Sitemap)) 97 | handleDefault("/", pub) 98 | 99 | go func() { 100 | sig := make(chan os.Signal, 1) 101 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) 102 | <-sig 103 | 104 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 105 | defer cancel() 106 | 107 | srv.Shutdown(ctx) 108 | }() 109 | 110 | logErr.Printf("Serving files in '%s' on %s\n", *dir, *addr) 111 | logErr.Println(srv.ListenAndServe()) 112 | 113 | fs.Close() 114 | } 115 | 116 | func orHyphen(s string) string { 117 | if s != "" { 118 | return s 119 | } 120 | return "-" 121 | } 122 | 123 | func checkMethod(han http.Handler) http.Handler { 124 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 125 | if r.Method != "HEAD" && r.Method != "GET" { 126 | http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) 127 | return 128 | } 129 | 130 | han.ServeHTTP(w, r) 131 | }) 132 | } 133 | 134 | func realIP(trustForward bool, han http.Handler) http.Handler { 135 | if !trustForward { 136 | return han 137 | } 138 | 139 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 140 | if realHost := r.Header.Get("X-Forwarded-Host"); realHost != "" { 141 | r.Host = realHost 142 | } 143 | if realIP := r.Header.Get("X-Real-IP"); realIP != "" { 144 | r.RemoteAddr = realIP + ":0" 145 | } 146 | 147 | han.ServeHTTP(w, r) 148 | }) 149 | } 150 | 151 | func nodir(han http.Handler) http.Handler { 152 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 153 | if r.URL.Path == "" || strings.HasSuffix(r.URL.Path, "/") { 154 | http.NotFound(w, r) 155 | return 156 | } 157 | 158 | han.ServeHTTP(w, r) 159 | }) 160 | } 161 | 162 | func logRequest(han http.Handler) http.Handler { 163 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 164 | u, _, _ := r.BasicAuth() 165 | h, _, _ := net.SplitHostPort(r.RemoteAddr) 166 | logOut.Printf("%.128s - %.256s [%s] %.2048q 0 0 %.2048q %.1024q\n", 167 | h, 168 | orHyphen(u), 169 | time.Now().Format("02/Jan/2006:15:04:05 -0700"), 170 | r.Method+" "+r.URL.String()+" "+r.Proto, 171 | orHyphen(r.Referer()), 172 | orHyphen(r.UserAgent()), 173 | ) 174 | han.ServeHTTP(w, r) 175 | }) 176 | } 177 | 178 | func logError(code int, err error, w http.ResponseWriter, r *http.Request) { 179 | h, _, _ := net.SplitHostPort(r.RemoteAddr) 180 | logErr.Printf("%.128s %.2048q %q\n", 181 | h, 182 | r.Method+" "+r.URL.String()+" "+r.Proto, 183 | err.Error(), 184 | ) 185 | 186 | http.Error(w, err.Error(), code) 187 | } 188 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nielsAD/autoindex/91a7b87c480cdb46ce24a6a3fd713a6d5e91a6b1/public/favicon.ico -------------------------------------------------------------------------------- /public/font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nielsAD/autoindex/91a7b87c480cdb46ce24a6a3fd713a6d5e91a6b1/public/font.woff -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Archive - toom.io 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /dl/ 3 | Sitemap: /urllist.txt 4 | -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const search = RegExp("[?&]q=([^&]+)"); 3 | function setPath(crumbs, files, q, path, query) { 4 | if (document.location.pathname != path || document.location.search != query) { 5 | history.pushState({}, document.title, path + query); 6 | path = document.location.pathname; 7 | } 8 | 9 | document.body.classList.add("loading"); 10 | window.scrollTo(0, 0); 11 | 12 | function a(sp, href, text, cls, rel) { 13 | let r = document.createElement("a"); 14 | r.appendChild(document.createTextNode(text.replace(/_/g, " "))); 15 | r.setAttribute("href", href); 16 | if (rel) r.setAttribute("rel", rel); 17 | if (cls) r.classList.add(cls); 18 | if (sp) r.addEventListener("click", function(e){ 19 | e.preventDefault(); 20 | setPath(crumbs, files, q, href, ""); 21 | }); 22 | return r; 23 | } 24 | function el(e, c) { 25 | let r = document.createElement(e); 26 | r.appendChild(c); 27 | return r; 28 | } 29 | 30 | path = path.replace(/\/\/+/g, "/").replace(/(^\/+)|(\/+$)/g, "") 31 | const p = (path)?path.split("/"):[]; 32 | let s = search.exec(query) 33 | let f = document.createDocumentFragment(); 34 | f.appendChild(el("li", a(true, "/", document.location.hostname))); 35 | let h = "/" 36 | for (let i = 0; i < p.length - (!s); i++) { 37 | h += p[i]; 38 | f.appendChild(el("li", a(true, h, decodeURIComponent(p[i])))); 39 | h += "/"; 40 | } 41 | 42 | if (s) { 43 | s = decodeURIComponent(s[1]); 44 | q.value = s; 45 | f.appendChild(el("li", el("span", document.createTextNode(s)))); 46 | } else { 47 | q.value = ""; 48 | f.appendChild(el("li", document.createTextNode(decodeURIComponent(p[p.length-1]||"")))); 49 | } 50 | crumbs.innerHTML = ""; 51 | crumbs.appendChild(f); 52 | 53 | const req = new XMLHttpRequest(); 54 | req.onreadystatechange = function() { 55 | if (this.readyState != 4) return; 56 | document.body.classList.remove("loading"); 57 | 58 | if (this.status != 200) { 59 | files.innerHTML="
  • "+((this.status == 404)?"Not found":"Load failed")+"
  • "; 60 | return; 61 | } 62 | 63 | let f = document.createDocumentFragment(); 64 | if (p[0] && !s) f.appendChild(el("li", a(true, "/"+p.slice(0,-1).join("/"), "..", "u"))); 65 | 66 | const json = JSON.parse(this.responseText || "[]"); 67 | for (let i = 0; i < json.length; i++) { 68 | const n = json[i].name 69 | const p = path+encodeURIComponent(json[i].name); 70 | if ((json[i].type||"")[0] == "f") 71 | f.appendChild(el("li", a(false, "/dl/"+p, n, "f", "nofollow"))); 72 | else 73 | f.appendChild(el("li", a(true, "/"+p.replace(/%2F/gi, "/"), n, "d", ""))); 74 | } 75 | 76 | if (f.childNodes.length) { 77 | files.innerHTML = ""; 78 | files.appendChild(f); 79 | } else { 80 | files.innerHTML="
  • No files found
  • "; 81 | } 82 | }; 83 | 84 | if (path) path += "/" 85 | req.open("GET", "/idx/" + path + query, true); 86 | req.send(); 87 | } 88 | function onLoad() { 89 | const path = document.getElementById("path"); 90 | const files = document.getElementById("files"); 91 | const search = document.getElementById("search"); 92 | const q = document.getElementById("q"); 93 | setPath(path, files, q, document.location.pathname, document.location.search); 94 | 95 | window.addEventListener("popstate", function(e) { 96 | setPath(path, files, q, document.location.pathname, document.location.search); 97 | }); 98 | 99 | search.addEventListener("submit", function(e) { 100 | e.preventDefault(); 101 | const s = q.value ? "?r=1&q=" + encodeURIComponent(q.value) : ""; 102 | setPath(path, files, q, document.location.pathname, s); 103 | }); 104 | } 105 | if (document.readyState === "loading") 106 | document.addEventListener("DOMContentLoaded", onLoad); 107 | else 108 | onLoad(); 109 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "icons"; 3 | src: url("/font.woff?201809121") format("woff"); 4 | } 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | } 9 | html { 10 | background-color: white; 11 | color: black; 12 | font-family: sans-serif; 13 | overflow-y: scroll; 14 | white-space: nowrap; 15 | } 16 | body { 17 | min-width: 250px; 18 | max-width: 980px; 19 | margin: 0 auto; 20 | } 21 | p { margin: 3px 0; } 22 | main { clear: both; } 23 | footer { text-align: center; } 24 | header { 25 | display: flex; 26 | justify-content: space-between; 27 | align-items: center; 28 | white-space: nowrap; 29 | margin: 1em 1em; 30 | } 31 | #path { 32 | list-style-type: none; 33 | display:flex; 34 | align-items: center; 35 | overflow: hidden; 36 | } 37 | #path li { 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | font-size: 70%; 41 | } 42 | #path li + li:before { 43 | content: "/"; 44 | margin: 0 0.3em; 45 | font-weight: initial; 46 | } 47 | #path li:first-child { flex-shrink: 0; } 48 | #path li:last-child { 49 | flex-shrink: 0; 50 | font-weight: bold; 51 | font-size: 100%; 52 | margin-right: 0.3em; 53 | } 54 | #path span { 55 | font-family: monospace; 56 | font-size: 130%; 57 | background-color: #eee9; 58 | } 59 | #search > * { 60 | line-height: 2em; 61 | padding: 6px; 62 | } 63 | #search input { 64 | width: 4em; 65 | color: #9999; 66 | border: 1px solid #eee9; 67 | text-overflow: ellipsis; 68 | transition: width 0.1s; 69 | } 70 | #search input:focus { 71 | color: black; 72 | width: 21em; 73 | } 74 | #search button { 75 | min-width: 2.5em; 76 | cursor: pointer; 77 | background-color: #eee9; 78 | border: 1px solid #eee9; 79 | } 80 | #search button span { 81 | display: inline-block; 82 | font-family: icons; 83 | transform: scale(1.5,1.5); 84 | } 85 | #files { 86 | list-style-type: none; 87 | margin: 0 1em 2em; 88 | padding: 1px; 89 | border: 1px solid #888; 90 | border-radius: 10px; 91 | overflow-x: auto; 92 | transition: opacity 0.1s; 93 | } 94 | #files li { padding: 5px 15px; } 95 | #files li:nth-child(even) { background-color: #eee9; } 96 | #files li:last-child { border-radius: 0 0 10px 10px; } 97 | #files li a { 98 | font-family: monospace; 99 | white-space: pre; 100 | } 101 | .u:before { content: "⬆"; } 102 | .d:before { content: "📁"; } 103 | .f:before { content: "📄"; } 104 | .u:before, .d:before, .f:before { 105 | font-family: icons; 106 | display: inline-block; 107 | padding: 0 7px 0 0; 108 | min-width: 1.2em; 109 | text-align: center; 110 | text-decoration: none; 111 | color: black; 112 | } 113 | .error { 114 | color: red; 115 | font-weight: bold; 116 | text-align: center; 117 | } 118 | .loading #path:after { 119 | content: " "; 120 | display: inline-block; 121 | border-radius: 50%; 122 | background-color: black; 123 | width: 0.7em; 124 | height: 0.7em; 125 | animation:blink normal 1s infinite ease-in-out; 126 | } 127 | .loading #files { opacity: 0.3; } 128 | @keyframes blink { 129 | 0%, 100% { transform: scale(0.3); opacity: 0.0; } 130 | 50% { transform: scale(1.0); opacity: 1.0; } 131 | } 132 | -------------------------------------------------------------------------------- /walk/dirent.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | package walk 6 | 7 | import ( 8 | "os" 9 | ) 10 | 11 | // Dirent stores a single directory entry (see getDirents) 12 | type Dirent struct { 13 | name string 14 | modeType os.FileMode 15 | } 16 | 17 | // Name of the directory entry 18 | func (d Dirent) Name() string { 19 | return d.name 20 | } 21 | 22 | // IsDir reports whether i describes a directory. 23 | func (d Dirent) IsDir() bool { 24 | return d.modeType&os.ModeDir != 0 25 | } 26 | 27 | // IsRegular reports whether i describes a regular file. 28 | func (d Dirent) IsRegular() bool { 29 | return d.modeType == 0 30 | } 31 | 32 | // IsSymlink reports whether i describes a symbolic link. 33 | func (d Dirent) IsSymlink() bool { 34 | return d.modeType&os.ModeSymlink != 0 35 | } 36 | -------------------------------------------------------------------------------- /walk/getdents_stdlib.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | //go:build !freebsd && !linux && !netbsd && !openbsd 6 | // +build !freebsd,!linux,!netbsd,!openbsd 7 | 8 | package walk 9 | 10 | import ( 11 | "os" 12 | ) 13 | 14 | func getdents(name string, _ []byte) ([]Dirent, error) { 15 | dir, err := os.ReadDir(name) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | res := make([]Dirent, len(dir)) 21 | for i, info := range dir { 22 | res[i] = Dirent{name: info.Name(), modeType: info.Type() & os.ModeType} 23 | } 24 | 25 | return res, nil 26 | } 27 | -------------------------------------------------------------------------------- /walk/getdents_unix.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | //go:build freebsd || linux || netbsd || openbsd 6 | // +build freebsd linux netbsd openbsd 7 | 8 | package walk 9 | 10 | import ( 11 | "bytes" 12 | "os" 13 | "path/filepath" 14 | "reflect" 15 | "syscall" 16 | "unsafe" 17 | ) 18 | 19 | func nameFromDent(de *syscall.Dirent) []byte { 20 | // Because this GOOS' syscall.Dirent does not provide a field that specifies 21 | // the name length, this function must first calculate the max possible name 22 | // length, and then search for the NULL byte. 23 | ml := int(uint64(de.Reclen) - uint64(unsafe.Offsetof(syscall.Dirent{}.Name))) 24 | 25 | // Convert syscall.Dirent.Name, which is array of int8, to []byte, by 26 | // overwriting Cap, Len, and Data slice header fields to values from 27 | // syscall.Dirent fields. Setting the Cap, Len, and Data field values for 28 | // the slice header modifies what the slice header points to, and in this 29 | // case, the name buffer. 30 | var name []byte 31 | sh := (*reflect.SliceHeader)(unsafe.Pointer(&name)) 32 | sh.Cap = ml 33 | sh.Len = ml 34 | sh.Data = uintptr(unsafe.Pointer(&de.Name[0])) 35 | 36 | if index := bytes.IndexByte(name, 0); index >= 0 { 37 | sh.Cap = index 38 | sh.Len = index 39 | return name 40 | } 41 | 42 | return nil 43 | } 44 | 45 | func getdents(name string, buf []byte) ([]Dirent, error) { 46 | dir, err := os.Open(name) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | fd := int(dir.Fd()) 52 | 53 | var res []Dirent 54 | for { 55 | n, err := syscall.Getdents(fd, buf) 56 | if err != nil { 57 | dir.Close() 58 | return nil, err 59 | } 60 | if n <= 0 { 61 | break 62 | } 63 | 64 | buf := buf[:n] 65 | for len(buf) > 0 { 66 | de := (*syscall.Dirent)(unsafe.Pointer(&buf[0])) 67 | buf = buf[de.Reclen:] 68 | 69 | if de.Ino == 0 { 70 | continue 71 | } 72 | 73 | nb := nameFromDent(de) 74 | nl := len(nb) 75 | if (nl == 0) || (nl == 1 && nb[0] == '.') || (nl == 2 && nb[0] == '.' && nb[1] == '.') { 76 | continue 77 | } 78 | entry := string(nb) 79 | 80 | var mode os.FileMode 81 | switch de.Type { 82 | case syscall.DT_REG: 83 | // regular file 84 | case syscall.DT_DIR: 85 | mode = os.ModeDir 86 | case syscall.DT_LNK: 87 | mode = os.ModeSymlink 88 | case syscall.DT_CHR: 89 | mode = os.ModeDevice | os.ModeCharDevice 90 | case syscall.DT_BLK: 91 | mode = os.ModeDevice 92 | case syscall.DT_FIFO: 93 | mode = os.ModeNamedPipe 94 | case syscall.DT_SOCK: 95 | mode = os.ModeSocket 96 | default: 97 | fi, err := os.Stat(filepath.Join(name, entry)) 98 | if err != nil { 99 | dir.Close() 100 | return nil, err 101 | } 102 | mode = fi.Mode() & os.ModeType 103 | } 104 | 105 | res = append(res, Dirent{name: entry, modeType: mode}) 106 | } 107 | } 108 | if err = dir.Close(); err != nil { 109 | return nil, err 110 | } 111 | return res, nil 112 | } 113 | -------------------------------------------------------------------------------- /walk/walk.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | // The code in this package is loosely based on https://github.com/karrick/godirwalk 6 | 7 | package walk 8 | 9 | import ( 10 | "errors" 11 | "os" 12 | "path/filepath" 13 | ) 14 | 15 | // Errors 16 | var ( 17 | ErrNonDir = errors.New("walk: Cannot iterate non-directory") 18 | ) 19 | 20 | // Options provide parameters for how the Walk function operates. 21 | type Options struct { 22 | // Invoked before entering a (sub)directory. 23 | Enter Visitor 24 | 25 | // Invoked for every encountered directory entry. 26 | Visit Visitor 27 | 28 | // Invoked after leaving a (sub)directory. 29 | Leave ErrorHandler 30 | 31 | // Invoked on error. 32 | Error ErrorHandler 33 | 34 | // ScratchBuffer is an optional byte slice to use as a scratch buffer. 35 | ScratchBuffer []byte 36 | } 37 | 38 | // Visitor callback function 39 | type Visitor func(dir string, entry *Dirent) error 40 | 41 | // ErrorHandler callback function 42 | type ErrorHandler func(dir string, entry *Dirent, err error) error 43 | 44 | // DefaultScratchBufferSize specifies the size of the scratch buffer that will be allocated by Walk 45 | const DefaultScratchBufferSize = 64 * 1024 46 | 47 | var minScratchBufferSize = os.Getpagesize() 48 | 49 | // Walk walks the file tree rooted at the specified directory, calling the 50 | // specified callback function for each file system node in the tree, including 51 | // root, symbolic links, and other node types. 52 | func Walk(root string, options *Options) error { 53 | root = filepath.Clean(root) 54 | 55 | fi, err := os.Stat(root) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | mode := fi.Mode() 61 | if mode&os.ModeDir == 0 { 62 | return ErrNonDir 63 | } 64 | 65 | dirent := Dirent{ 66 | name: filepath.Base(root), 67 | modeType: mode & os.ModeType, 68 | } 69 | 70 | if options.Enter == nil { 71 | options.Enter = defVisit 72 | } 73 | if options.Visit == nil { 74 | options.Visit = defVisit 75 | } 76 | if options.Leave == nil { 77 | options.Leave = defError 78 | } 79 | if options.Error == nil { 80 | options.Error = defError 81 | } 82 | 83 | if len(options.ScratchBuffer) < minScratchBufferSize { 84 | options.ScratchBuffer = make([]byte, DefaultScratchBufferSize) 85 | } 86 | 87 | return walk(root, &dirent, options) 88 | } 89 | 90 | func defVisit(dir string, entry *Dirent) error { return nil } 91 | func defError(dir string, entry *Dirent, err error) error { return err } 92 | 93 | func walk(path string, dirent *Dirent, options *Options) error { 94 | if dirent.IsSymlink() { 95 | if !dirent.IsDir() { 96 | ref, err := os.Readlink(path) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | if !filepath.IsAbs(ref) { 102 | ref = filepath.Join(filepath.Dir(path), ref) 103 | } 104 | 105 | s, err := os.Stat(ref) 106 | if err != nil { 107 | return err 108 | } 109 | dirent.modeType = (s.Mode() & os.ModeType) | os.ModeSymlink 110 | } 111 | } 112 | 113 | err := options.Visit(path, dirent) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if !dirent.IsDir() { 119 | return nil 120 | } 121 | 122 | if err := options.Enter(path, dirent); err != nil { 123 | if err == filepath.SkipDir { 124 | return nil 125 | } 126 | return err 127 | } 128 | 129 | ents, err := getdents(path, options.ScratchBuffer) 130 | if err != nil { 131 | err = options.Error(path, dirent, err) 132 | goto leave 133 | } 134 | 135 | for i := range ents { 136 | child := filepath.Join(path, ents[i].name) 137 | err = walk(child, &ents[i], options) 138 | if err == nil { 139 | continue 140 | } 141 | if err == filepath.SkipDir { 142 | break 143 | } 144 | 145 | err = options.Error(child, &ents[i], err) 146 | if err != nil { 147 | break 148 | } 149 | } 150 | 151 | leave: 152 | if err == filepath.SkipDir { 153 | err = nil 154 | } 155 | return options.Leave(path, dirent, err) 156 | } 157 | -------------------------------------------------------------------------------- /walk/walk_test.go: -------------------------------------------------------------------------------- 1 | // Author: Niels A.D. 2 | // Project: autoindex (https://github.com/nielsAD/autoindex) 3 | // License: Mozilla Public License, v2.0 4 | 5 | package walk_test 6 | 7 | import ( 8 | "reflect" 9 | "sort" 10 | "testing" 11 | 12 | "github.com/nielsAD/autoindex/walk" 13 | ) 14 | 15 | func TestWalk(t *testing.T) { 16 | var expected = []string{".", "dirent.go", "getdents_stdlib.go", "getdents_unix.go", "walk.go", "walk_test.go"} 17 | 18 | var files []string 19 | walk.Walk(".", &walk.Options{ 20 | Visit: func(dir string, entry *walk.Dirent) error { 21 | files = append(files, entry.Name()) 22 | return nil 23 | }, 24 | Error: func(dir string, entry *walk.Dirent, err error) error { 25 | t.Errorf("Error walking `%s`: %s\n", dir, err.Error()) 26 | return err 27 | }, 28 | }) 29 | 30 | sort.Strings(files) 31 | if !reflect.DeepEqual(files, expected) { 32 | t.Errorf("Unexpected directory contents: %s\n", files) 33 | } 34 | } 35 | --------------------------------------------------------------------------------