init
This commit is contained in:
		
						commit
						49b24e5773
					
				
					 22 changed files with 1499 additions and 0 deletions
				
			
		
							
								
								
									
										14
									
								
								.env.example
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.env.example
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | |||
| # the storage driver used | ||||
| # - local: stores files on a local hard drive | ||||
| # - s3: uses S3-compatible storage (not yet implemented) | ||||
| STORAGE=local | ||||
| 
 | ||||
| # for local storage, the path used | ||||
| # if the path starts with a /, it's treated as an absolute path | ||||
| # if not, it's treated as a path relative to the working directory | ||||
| # defaults to "uploads" | ||||
| LOCAL_PATH=/var/filer/uploads | ||||
| 
 | ||||
| # the port used by the web server | ||||
| # the server will not run if this is empty | ||||
| PORT=8080 | ||||
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| .env | ||||
| *.db | ||||
| uploads/ | ||||
							
								
								
									
										201
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,201 @@ | |||
|                                  Apache License | ||||
|                            Version 2.0, January 2004 | ||||
|                         http://www.apache.org/licenses/ | ||||
| 
 | ||||
|    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||
| 
 | ||||
|    1. Definitions. | ||||
| 
 | ||||
|       "License" shall mean the terms and conditions for use, reproduction, | ||||
|       and distribution as defined by Sections 1 through 9 of this document. | ||||
| 
 | ||||
|       "Licensor" shall mean the copyright owner or entity authorized by | ||||
|       the copyright owner that is granting the License. | ||||
| 
 | ||||
|       "Legal Entity" shall mean the union of the acting entity and all | ||||
|       other entities that control, are controlled by, or are under common | ||||
|       control with that entity. For the purposes of this definition, | ||||
|       "control" means (i) the power, direct or indirect, to cause the | ||||
|       direction or management of such entity, whether by contract or | ||||
|       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||
|       outstanding shares, or (iii) beneficial ownership of such entity. | ||||
| 
 | ||||
|       "You" (or "Your") shall mean an individual or Legal Entity | ||||
|       exercising permissions granted by this License. | ||||
| 
 | ||||
|       "Source" form shall mean the preferred form for making modifications, | ||||
|       including but not limited to software source code, documentation | ||||
|       source, and configuration files. | ||||
| 
 | ||||
|       "Object" form shall mean any form resulting from mechanical | ||||
|       transformation or translation of a Source form, including but | ||||
|       not limited to compiled object code, generated documentation, | ||||
|       and conversions to other media types. | ||||
| 
 | ||||
|       "Work" shall mean the work of authorship, whether in Source or | ||||
|       Object form, made available under the License, as indicated by a | ||||
|       copyright notice that is included in or attached to the work | ||||
|       (an example is provided in the Appendix below). | ||||
| 
 | ||||
|       "Derivative Works" shall mean any work, whether in Source or Object | ||||
|       form, that is based on (or derived from) the Work and for which the | ||||
|       editorial revisions, annotations, elaborations, or other modifications | ||||
|       represent, as a whole, an original work of authorship. For the purposes | ||||
|       of this License, Derivative Works shall not include works that remain | ||||
|       separable from, or merely link (or bind by name) to the interfaces of, | ||||
|       the Work and Derivative Works thereof. | ||||
| 
 | ||||
|       "Contribution" shall mean any work of authorship, including | ||||
|       the original version of the Work and any modifications or additions | ||||
|       to that Work or Derivative Works thereof, that is intentionally | ||||
|       submitted to Licensor for inclusion in the Work by the copyright owner | ||||
|       or by an individual or Legal Entity authorized to submit on behalf of | ||||
|       the copyright owner. For the purposes of this definition, "submitted" | ||||
|       means any form of electronic, verbal, or written communication sent | ||||
|       to the Licensor or its representatives, including but not limited to | ||||
|       communication on electronic mailing lists, source code control systems, | ||||
|       and issue tracking systems that are managed by, or on behalf of, the | ||||
|       Licensor for the purpose of discussing and improving the Work, but | ||||
|       excluding communication that is conspicuously marked or otherwise | ||||
|       designated in writing by the copyright owner as "Not a Contribution." | ||||
| 
 | ||||
|       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||
|       on behalf of whom a Contribution has been received by Licensor and | ||||
|       subsequently incorporated within the Work. | ||||
| 
 | ||||
|    2. Grant of Copyright License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       copyright license to reproduce, prepare Derivative Works of, | ||||
|       publicly display, publicly perform, sublicense, and distribute the | ||||
|       Work and such Derivative Works in Source or Object form. | ||||
| 
 | ||||
|    3. Grant of Patent License. Subject to the terms and conditions of | ||||
|       this License, each Contributor hereby grants to You a perpetual, | ||||
|       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||
|       (except as stated in this section) patent license to make, have made, | ||||
|       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||
|       where such license applies only to those patent claims licensable | ||||
|       by such Contributor that are necessarily infringed by their | ||||
|       Contribution(s) alone or by combination of their Contribution(s) | ||||
|       with the Work to which such Contribution(s) was submitted. If You | ||||
|       institute patent litigation against any entity (including a | ||||
|       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||
|       or a Contribution incorporated within the Work constitutes direct | ||||
|       or contributory patent infringement, then any patent licenses | ||||
|       granted to You under this License for that Work shall terminate | ||||
|       as of the date such litigation is filed. | ||||
| 
 | ||||
|    4. Redistribution. You may reproduce and distribute copies of the | ||||
|       Work or Derivative Works thereof in any medium, with or without | ||||
|       modifications, and in Source or Object form, provided that You | ||||
|       meet the following conditions: | ||||
| 
 | ||||
|       (a) You must give any other recipients of the Work or | ||||
|           Derivative Works a copy of this License; and | ||||
| 
 | ||||
|       (b) You must cause any modified files to carry prominent notices | ||||
|           stating that You changed the files; and | ||||
| 
 | ||||
|       (c) You must retain, in the Source form of any Derivative Works | ||||
|           that You distribute, all copyright, patent, trademark, and | ||||
|           attribution notices from the Source form of the Work, | ||||
|           excluding those notices that do not pertain to any part of | ||||
|           the Derivative Works; and | ||||
| 
 | ||||
|       (d) If the Work includes a "NOTICE" text file as part of its | ||||
|           distribution, then any Derivative Works that You distribute must | ||||
|           include a readable copy of the attribution notices contained | ||||
|           within such NOTICE file, excluding those notices that do not | ||||
|           pertain to any part of the Derivative Works, in at least one | ||||
|           of the following places: within a NOTICE text file distributed | ||||
|           as part of the Derivative Works; within the Source form or | ||||
|           documentation, if provided along with the Derivative Works; or, | ||||
|           within a display generated by the Derivative Works, if and | ||||
|           wherever such third-party notices normally appear. The contents | ||||
|           of the NOTICE file are for informational purposes only and | ||||
|           do not modify the License. You may add Your own attribution | ||||
|           notices within Derivative Works that You distribute, alongside | ||||
|           or as an addendum to the NOTICE text from the Work, provided | ||||
|           that such additional attribution notices cannot be construed | ||||
|           as modifying the License. | ||||
| 
 | ||||
|       You may add Your own copyright statement to Your modifications and | ||||
|       may provide additional or different license terms and conditions | ||||
|       for use, reproduction, or distribution of Your modifications, or | ||||
|       for any such Derivative Works as a whole, provided Your use, | ||||
|       reproduction, and distribution of the Work otherwise complies with | ||||
|       the conditions stated in this License. | ||||
| 
 | ||||
|    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||
|       any Contribution intentionally submitted for inclusion in the Work | ||||
|       by You to the Licensor shall be under the terms and conditions of | ||||
|       this License, without any additional terms or conditions. | ||||
|       Notwithstanding the above, nothing herein shall supersede or modify | ||||
|       the terms of any separate license agreement you may have executed | ||||
|       with Licensor regarding such Contributions. | ||||
| 
 | ||||
|    6. Trademarks. This License does not grant permission to use the trade | ||||
|       names, trademarks, service marks, or product names of the Licensor, | ||||
|       except as required for reasonable and customary use in describing the | ||||
|       origin of the Work and reproducing the content of the NOTICE file. | ||||
| 
 | ||||
|    7. Disclaimer of Warranty. Unless required by applicable law or | ||||
|       agreed to in writing, Licensor provides the Work (and each | ||||
|       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||
|       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||
|       implied, including, without limitation, any warranties or conditions | ||||
|       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||
|       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||
|       appropriateness of using or redistributing the Work and assume any | ||||
|       risks associated with Your exercise of permissions under this License. | ||||
| 
 | ||||
|    8. Limitation of Liability. In no event and under no legal theory, | ||||
|       whether in tort (including negligence), contract, or otherwise, | ||||
|       unless required by applicable law (such as deliberate and grossly | ||||
|       negligent acts) or agreed to in writing, shall any Contributor be | ||||
|       liable to You for damages, including any direct, indirect, special, | ||||
|       incidental, or consequential damages of any character arising as a | ||||
|       result of this License or out of the use or inability to use the | ||||
|       Work (including but not limited to damages for loss of goodwill, | ||||
|       work stoppage, computer failure or malfunction, or any and all | ||||
|       other commercial damages or losses), even if such Contributor | ||||
|       has been advised of the possibility of such damages. | ||||
| 
 | ||||
|    9. Accepting Warranty or Additional Liability. While redistributing | ||||
|       the Work or Derivative Works thereof, You may choose to offer, | ||||
|       and charge a fee for, acceptance of support, warranty, indemnity, | ||||
|       or other liability obligations and/or rights consistent with this | ||||
|       License. However, in accepting such obligations, You may act only | ||||
|       on Your own behalf and on Your sole responsibility, not on behalf | ||||
|       of any other Contributor, and only if You agree to indemnify, | ||||
|       defend, and hold each Contributor harmless for any liability | ||||
|       incurred by, or claims asserted against, such Contributor by reason | ||||
|       of your accepting any such warranty or additional liability. | ||||
| 
 | ||||
|    END OF TERMS AND CONDITIONS | ||||
| 
 | ||||
|    APPENDIX: How to apply the Apache License to your work. | ||||
| 
 | ||||
|       To apply the Apache License to your work, attach the following | ||||
|       boilerplate notice, with the fields enclosed by brackets "[]" | ||||
|       replaced with your own identifying information. (Don't include | ||||
|       the brackets!)  The text should be enclosed in the appropriate | ||||
|       comment syntax for the file format. We also recommend that a | ||||
|       file or class name and description of purpose be included on the | ||||
|       same "printed page" as the copyright notice for easier | ||||
|       identification within third-party archives. | ||||
| 
 | ||||
|    Copyright [yyyy] [name of copyright owner] | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
							
								
								
									
										49
									
								
								cmd/users/users.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								cmd/users/users.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| package users | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/db" | ||||
| 	"github.com/olekukonko/tablewriter" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| 
 | ||||
| var Command = &cli.Command{ | ||||
| 	Name:   "users", | ||||
| 	Usage:  "Manage user accounts", | ||||
| 	Action: run, | ||||
| } | ||||
| 
 | ||||
| func run(c *cli.Context) error { | ||||
| 	db, err := db.New() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	users, err := db.GetUsers(c.Context) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	table := tablewriter.NewWriter(c.App.Writer) | ||||
| 	table.SetHeader([]string{"ID", "Username", "Admin?"}) | ||||
| 	table.SetBorder(false) | ||||
| 
 | ||||
| 	for _, u := range users { | ||||
| 		table.Append([]string{ | ||||
| 			fmt.Sprint(u.ID), | ||||
| 			u.Username, | ||||
| 			yesNo(u.IsAdmin), | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	table.Render() | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func yesNo(b bool) string { | ||||
| 	if b { | ||||
| 		return "yes" | ||||
| 	} | ||||
| 	return "no" | ||||
| } | ||||
							
								
								
									
										24
									
								
								cmd/web/web.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								cmd/web/web.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | |||
| package web | ||||
| 
 | ||||
| import ( | ||||
| 	"codeberg.org/u1f320/filer/db" | ||||
| 	"codeberg.org/u1f320/filer/web" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| 
 | ||||
| var Command = &cli.Command{ | ||||
| 	Name:   "web", | ||||
| 	Usage:  "Serve the web server", | ||||
| 	Action: run, | ||||
| } | ||||
| 
 | ||||
| func run(c *cli.Context) error { | ||||
| 	db, err := db.New() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	app := web.New(db) | ||||
| 	app.Run(c.Context) | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										78
									
								
								db/db.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								db/db.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,78 @@ | |||
| package db | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"embed" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/db/queries" | ||||
| 	"codeberg.org/u1f320/filer/store" | ||||
| 	"codeberg.org/u1f320/filer/store/local" | ||||
| 	"emperror.dev/errors" | ||||
| 	"github.com/joho/godotenv" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	migrate "github.com/rubenv/sql-migrate" | ||||
| 
 | ||||
| 	// sqlite driver | ||||
| 	_ "modernc.org/sqlite" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
| 	godotenv.Load() | ||||
| } | ||||
| 
 | ||||
| const defaultDBName = "filer.db?_pragma=foreign_keys(1)" | ||||
| 
 | ||||
| type DB struct { | ||||
| 	*queries.Queries | ||||
| 
 | ||||
| 	db    *sql.DB | ||||
| 	Store store.Store | ||||
| } | ||||
| 
 | ||||
| func New() (*DB, error) { | ||||
| 	sqldb, err := sql.Open("sqlite", defaultDBName) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "creating database") | ||||
| 	} | ||||
| 
 | ||||
| 	err = migrateDB(sqldb, defaultDBName) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "migrating database") | ||||
| 	} | ||||
| 
 | ||||
| 	db := &DB{ | ||||
| 		Queries: queries.New(sqldb), | ||||
| 		db:      sqldb, | ||||
| 	} | ||||
| 
 | ||||
| 	switch os.Getenv("STORAGE") { | ||||
| 	case local.StoreKey: | ||||
| 		db.Store, err = local.New(os.Getenv("LOCAL_PATH")) | ||||
| 	default: | ||||
| 		err = store.ErrInvalidStoreKey | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "creating store") | ||||
| 	} | ||||
| 
 | ||||
| 	return db, nil | ||||
| } | ||||
| 
 | ||||
| //go:embed migrations | ||||
| var migrateFS embed.FS | ||||
| 
 | ||||
| func migrateDB(db *sql.DB, dbName string) error { | ||||
| 	migrations := &migrate.EmbedFileSystemMigrationSource{ | ||||
| 		FileSystem: migrateFS, | ||||
| 		Root:       "migrations", | ||||
| 	} | ||||
| 
 | ||||
| 	n, err := migrate.Exec(db, "sqlite3", migrations, migrate.Up) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "executing migrations") | ||||
| 	} | ||||
| 
 | ||||
| 	log.Info().Int("count", n).Msg("Performed migrations!") | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										33
									
								
								db/migrations/1692914919-init.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								db/migrations/1692914919-init.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| -- +migrate Up | ||||
| 
 | ||||
| create table users ( | ||||
|     id       integer primary key, | ||||
|     username text not null unique, | ||||
|     password text not null, | ||||
|     is_admin boolean not null default false | ||||
| ); | ||||
| 
 | ||||
| create table tokens ( | ||||
|     id      integer primary key, | ||||
|     user_id integer not null references users (id) on delete cascade, | ||||
|     token   text not null unique | ||||
| ); | ||||
| 
 | ||||
| create table files ( | ||||
|     id           text    primary key, -- uuid | ||||
|     user_id      integer not null references users (id) on delete cascade, | ||||
|     filename     text not null, | ||||
|     content_type text not null, | ||||
|     hash         text not null, | ||||
|     size         integer not null, | ||||
|     created_at   integer not null, | ||||
|     expires      integer, | ||||
| 
 | ||||
|     unique(filename, hash) | ||||
| ); | ||||
| 
 | ||||
| -- +migrate Down | ||||
| 
 | ||||
| drop table files; | ||||
| drop table tokens; | ||||
| drop table users; | ||||
							
								
								
									
										47
									
								
								db/queries.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								db/queries.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| -- name: CreateUser :one | ||||
| insert into users (username, password) values (@username, @password) | ||||
| returning *; | ||||
| 
 | ||||
| -- name: GetUser :one | ||||
| select * from users where username = @username; | ||||
| 
 | ||||
| -- name: GetUsers :many | ||||
| select * from users order by id; | ||||
| 
 | ||||
| -- name: UpdateUser :one | ||||
| update users set | ||||
| username = @username, password = @password, is_admin = @is_admin | ||||
| where id = @id | ||||
| returning *; | ||||
| 
 | ||||
| -- name: DeleteUser :exec | ||||
| delete from users where id = @id; | ||||
| 
 | ||||
| -- name: GetUserByToken :one | ||||
| select u.* from users u join tokens t on u.id = t.user_id | ||||
| where t.token = @token; | ||||
| 
 | ||||
| -- name: CreateToken :one | ||||
| insert into tokens (user_id, token) values (@user_id, @token) | ||||
| returning *; | ||||
| 
 | ||||
| -- name: DeleteToken :exec | ||||
| delete from tokens where id = @id; | ||||
| 
 | ||||
| -- name: CreateFile :one | ||||
| insert into files | ||||
| (id, user_id, filename, content_type, hash, size, created_at, expires) | ||||
| values (@id, @user_id, @filename, @content_type, @hash, @size, unixepoch(), @expires) | ||||
| returning *; | ||||
| 
 | ||||
| -- name: GetFileByID :one | ||||
| select * from files where id = @id; | ||||
| 
 | ||||
| -- name: GetFileByName :one | ||||
| select * from files where filename = @filename and hash = @hash; | ||||
| 
 | ||||
| -- name: GetExpiredFiles :many | ||||
| select * from files where expires < unixepoch(); | ||||
| 
 | ||||
| -- name: DeleteFile :exec | ||||
| delete from files where id = @id; | ||||
							
								
								
									
										31
									
								
								db/queries/db.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								db/queries/db.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| // Code generated by sqlc. DO NOT EDIT. | ||||
| // versions: | ||||
| //   sqlc v1.20.0 | ||||
| 
 | ||||
| package queries | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| ) | ||||
| 
 | ||||
| type DBTX interface { | ||||
| 	ExecContext(context.Context, string, ...interface{}) (sql.Result, error) | ||||
| 	PrepareContext(context.Context, string) (*sql.Stmt, error) | ||||
| 	QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) | ||||
| 	QueryRowContext(context.Context, string, ...interface{}) *sql.Row | ||||
| } | ||||
| 
 | ||||
| func New(db DBTX) *Queries { | ||||
| 	return &Queries{db: db} | ||||
| } | ||||
| 
 | ||||
| type Queries struct { | ||||
| 	db DBTX | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) WithTx(tx *sql.Tx) *Queries { | ||||
| 	return &Queries{ | ||||
| 		db: tx, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										35
									
								
								db/queries/models.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								db/queries/models.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | |||
| // Code generated by sqlc. DO NOT EDIT. | ||||
| // versions: | ||||
| //   sqlc v1.20.0 | ||||
| 
 | ||||
| package queries | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| ) | ||||
| 
 | ||||
| type File struct { | ||||
| 	ID          uuid.UUID | ||||
| 	UserID      int64 | ||||
| 	Filename    string | ||||
| 	ContentType string | ||||
| 	Hash        string | ||||
| 	Size        int64 | ||||
| 	CreatedAt   int64 | ||||
| 	Expires     sql.NullInt64 | ||||
| } | ||||
| 
 | ||||
| type Token struct { | ||||
| 	ID     int64 | ||||
| 	UserID int64 | ||||
| 	Token  string | ||||
| } | ||||
| 
 | ||||
| type User struct { | ||||
| 	ID       int64 | ||||
| 	Username string | ||||
| 	Password string | ||||
| 	IsAdmin  bool | ||||
| } | ||||
							
								
								
									
										297
									
								
								db/queries/queries.sql.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								db/queries/queries.sql.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,297 @@ | |||
| // Code generated by sqlc. DO NOT EDIT. | ||||
| // versions: | ||||
| //   sqlc v1.20.0 | ||||
| // source: queries.sql | ||||
| 
 | ||||
| package queries | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"database/sql" | ||||
| 
 | ||||
| 	"github.com/google/uuid" | ||||
| ) | ||||
| 
 | ||||
| const createFile = `-- name: CreateFile :one | ||||
| insert into files | ||||
| (id, user_id, filename, content_type, hash, size, created_at, expires) | ||||
| values (?1, ?2, ?3, ?4, ?5, ?6, unixepoch(), ?7) | ||||
| returning id, user_id, filename, content_type, hash, size, created_at, expires | ||||
| ` | ||||
| 
 | ||||
| type CreateFileParams struct { | ||||
| 	ID          uuid.UUID | ||||
| 	UserID      int64 | ||||
| 	Filename    string | ||||
| 	ContentType string | ||||
| 	Hash        string | ||||
| 	Size        int64 | ||||
| 	Expires     sql.NullInt64 | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, createFile, | ||||
| 		arg.ID, | ||||
| 		arg.UserID, | ||||
| 		arg.Filename, | ||||
| 		arg.ContentType, | ||||
| 		arg.Hash, | ||||
| 		arg.Size, | ||||
| 		arg.Expires, | ||||
| 	) | ||||
| 	var i File | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.UserID, | ||||
| 		&i.Filename, | ||||
| 		&i.ContentType, | ||||
| 		&i.Hash, | ||||
| 		&i.Size, | ||||
| 		&i.CreatedAt, | ||||
| 		&i.Expires, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const createToken = `-- name: CreateToken :one | ||||
| insert into tokens (user_id, token) values (?1, ?2) | ||||
| returning id, user_id, token | ||||
| ` | ||||
| 
 | ||||
| type CreateTokenParams struct { | ||||
| 	UserID int64 | ||||
| 	Token  string | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) CreateToken(ctx context.Context, arg CreateTokenParams) (Token, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, createToken, arg.UserID, arg.Token) | ||||
| 	var i Token | ||||
| 	err := row.Scan(&i.ID, &i.UserID, &i.Token) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const createUser = `-- name: CreateUser :one | ||||
| insert into users (username, password) values (?1, ?2) | ||||
| returning id, username, password, is_admin | ||||
| ` | ||||
| 
 | ||||
| type CreateUserParams struct { | ||||
| 	Username string | ||||
| 	Password string | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, createUser, arg.Username, arg.Password) | ||||
| 	var i User | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.Username, | ||||
| 		&i.Password, | ||||
| 		&i.IsAdmin, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const deleteFile = `-- name: DeleteFile :exec | ||||
| delete from files where id = ?1 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) DeleteFile(ctx context.Context, id uuid.UUID) error { | ||||
| 	_, err := q.db.ExecContext(ctx, deleteFile, id) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| const deleteToken = `-- name: DeleteToken :exec | ||||
| delete from tokens where id = ?1 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) DeleteToken(ctx context.Context, id int64) error { | ||||
| 	_, err := q.db.ExecContext(ctx, deleteToken, id) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| const deleteUser = `-- name: DeleteUser :exec | ||||
| delete from users where id = ?1 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) DeleteUser(ctx context.Context, id int64) error { | ||||
| 	_, err := q.db.ExecContext(ctx, deleteUser, id) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| const getExpiredFiles = `-- name: GetExpiredFiles :many | ||||
| select id, user_id, filename, content_type, hash, size, created_at, expires from files where expires < unixepoch() | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) GetExpiredFiles(ctx context.Context) ([]File, error) { | ||||
| 	rows, err := q.db.QueryContext(ctx, getExpiredFiles) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 	var items []File | ||||
| 	for rows.Next() { | ||||
| 		var i File | ||||
| 		if err := rows.Scan( | ||||
| 			&i.ID, | ||||
| 			&i.UserID, | ||||
| 			&i.Filename, | ||||
| 			&i.ContentType, | ||||
| 			&i.Hash, | ||||
| 			&i.Size, | ||||
| 			&i.CreatedAt, | ||||
| 			&i.Expires, | ||||
| 		); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		items = append(items, i) | ||||
| 	} | ||||
| 	if err := rows.Close(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := rows.Err(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return items, nil | ||||
| } | ||||
| 
 | ||||
| const getFileByID = `-- name: GetFileByID :one | ||||
| select id, user_id, filename, content_type, hash, size, created_at, expires from files where id = ?1 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) GetFileByID(ctx context.Context, id uuid.UUID) (File, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, getFileByID, id) | ||||
| 	var i File | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.UserID, | ||||
| 		&i.Filename, | ||||
| 		&i.ContentType, | ||||
| 		&i.Hash, | ||||
| 		&i.Size, | ||||
| 		&i.CreatedAt, | ||||
| 		&i.Expires, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const getFileByName = `-- name: GetFileByName :one | ||||
| select id, user_id, filename, content_type, hash, size, created_at, expires from files where filename = ?1 and hash = ?2 | ||||
| ` | ||||
| 
 | ||||
| type GetFileByNameParams struct { | ||||
| 	Filename string | ||||
| 	Hash     string | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) GetFileByName(ctx context.Context, arg GetFileByNameParams) (File, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, getFileByName, arg.Filename, arg.Hash) | ||||
| 	var i File | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.UserID, | ||||
| 		&i.Filename, | ||||
| 		&i.ContentType, | ||||
| 		&i.Hash, | ||||
| 		&i.Size, | ||||
| 		&i.CreatedAt, | ||||
| 		&i.Expires, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const getUser = `-- name: GetUser :one | ||||
| select id, username, password, is_admin from users where username = ?1 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) GetUser(ctx context.Context, username string) (User, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, getUser, username) | ||||
| 	var i User | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.Username, | ||||
| 		&i.Password, | ||||
| 		&i.IsAdmin, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const getUserByToken = `-- name: GetUserByToken :one | ||||
| select u.id, u.username, u.password, u.is_admin from users u join tokens t on u.id = t.user_id | ||||
| where t.token = ?1 | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) GetUserByToken(ctx context.Context, token string) (User, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, getUserByToken, token) | ||||
| 	var i User | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.Username, | ||||
| 		&i.Password, | ||||
| 		&i.IsAdmin, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
| 
 | ||||
| const getUsers = `-- name: GetUsers :many | ||||
| select id, username, password, is_admin from users order by id | ||||
| ` | ||||
| 
 | ||||
| func (q *Queries) GetUsers(ctx context.Context) ([]User, error) { | ||||
| 	rows, err := q.db.QueryContext(ctx, getUsers) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 	var items []User | ||||
| 	for rows.Next() { | ||||
| 		var i User | ||||
| 		if err := rows.Scan( | ||||
| 			&i.ID, | ||||
| 			&i.Username, | ||||
| 			&i.Password, | ||||
| 			&i.IsAdmin, | ||||
| 		); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		items = append(items, i) | ||||
| 	} | ||||
| 	if err := rows.Close(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := rows.Err(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return items, nil | ||||
| } | ||||
| 
 | ||||
| const updateUser = `-- name: UpdateUser :one | ||||
| update users set | ||||
| username = ?1, password = ?2, is_admin = ?3 | ||||
| where id = ?4 | ||||
| returning id, username, password, is_admin | ||||
| ` | ||||
| 
 | ||||
| type UpdateUserParams struct { | ||||
| 	Username string | ||||
| 	Password string | ||||
| 	IsAdmin  bool | ||||
| 	ID       int64 | ||||
| } | ||||
| 
 | ||||
| func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { | ||||
| 	row := q.db.QueryRowContext(ctx, updateUser, | ||||
| 		arg.Username, | ||||
| 		arg.Password, | ||||
| 		arg.IsAdmin, | ||||
| 		arg.ID, | ||||
| 	) | ||||
| 	var i User | ||||
| 	err := row.Scan( | ||||
| 		&i.ID, | ||||
| 		&i.Username, | ||||
| 		&i.Password, | ||||
| 		&i.IsAdmin, | ||||
| 	) | ||||
| 	return i, err | ||||
| } | ||||
							
								
								
									
										25
									
								
								db/schema.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								db/schema.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| create table users ( | ||||
|     id       integer primary key, | ||||
|     username text not null unique, | ||||
|     password text not null, | ||||
|     is_admin boolean not null default false | ||||
| ); | ||||
| 
 | ||||
| create table tokens ( | ||||
|     id      integer primary key, | ||||
|     user_id integer not null references users (id) on delete cascade, | ||||
|     token   text not null unique | ||||
| ); | ||||
| 
 | ||||
| create table files ( | ||||
|     id           text    primary key, -- uuid | ||||
|     user_id      integer not null references users (id) on delete cascade, | ||||
|     filename     text not null, | ||||
|     content_type text not null, | ||||
|     hash         text not null, | ||||
|     size         integer not null, | ||||
|     created_at   integer not null, | ||||
|     expires      integer, | ||||
| 
 | ||||
|     unique(filename, hash) | ||||
| ); | ||||
							
								
								
									
										48
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| module codeberg.org/u1f320/filer | ||||
| 
 | ||||
| go 1.20 | ||||
| 
 | ||||
| require ( | ||||
| 	emperror.dev/errors v0.8.1 | ||||
| 	github.com/go-chi/chi/v5 v5.0.10 | ||||
| 	github.com/go-chi/cors v1.2.1 | ||||
| 	github.com/google/uuid v1.3.0 | ||||
| 	github.com/joho/godotenv v1.5.1 | ||||
| 	github.com/olekukonko/tablewriter v0.0.5 | ||||
| 	github.com/rs/zerolog v1.30.0 | ||||
| 	github.com/rubenv/sql-migrate v1.5.2 | ||||
| 	github.com/urfave/cli/v2 v2.25.7 | ||||
| 	modernc.org/sqlite v1.18.1 | ||||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect | ||||
| 	github.com/go-gorp/gorp/v3 v3.1.0 // indirect | ||||
| 	github.com/google/go-cmp v0.5.9 // indirect | ||||
| 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.17 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.9 // indirect | ||||
| 	github.com/mattn/go-sqlite3 v1.14.16 // indirect | ||||
| 	github.com/pkg/errors v0.9.1 // indirect | ||||
| 	github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect | ||||
| 	github.com/russross/blackfriday/v2 v2.1.0 // indirect | ||||
| 	github.com/sirupsen/logrus v1.9.2 // indirect | ||||
| 	github.com/stretchr/testify v1.8.1 // indirect | ||||
| 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect | ||||
| 	go.uber.org/atomic v1.7.0 // indirect | ||||
| 	go.uber.org/multierr v1.6.0 // indirect | ||||
| 	golang.org/x/mod v0.10.0 // indirect | ||||
| 	golang.org/x/sys v0.8.0 // indirect | ||||
| 	golang.org/x/term v0.8.0 // indirect | ||||
| 	golang.org/x/tools v0.9.1 // indirect | ||||
| 	lukechampine.com/uint128 v1.2.0 // indirect | ||||
| 	modernc.org/cc/v3 v3.36.3 // indirect | ||||
| 	modernc.org/ccgo/v3 v3.16.9 // indirect | ||||
| 	modernc.org/libc v1.17.1 // indirect | ||||
| 	modernc.org/mathutil v1.5.0 // indirect | ||||
| 	modernc.org/memory v1.2.1 // indirect | ||||
| 	modernc.org/opt v0.1.3 // indirect | ||||
| 	modernc.org/strutil v1.1.3 // indirect | ||||
| 	modernc.org/token v1.0.0 // indirect | ||||
| ) | ||||
							
								
								
									
										161
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,161 @@ | |||
| emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= | ||||
| emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= | ||||
| github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= | ||||
| github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= | ||||
| github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= | ||||
| github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | ||||
| github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= | ||||
| github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= | ||||
| github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= | ||||
| github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= | ||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | ||||
| github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= | ||||
| github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= | ||||
| github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= | ||||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||||
| github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||||
| github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||||
| github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= | ||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= | ||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | ||||
| github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||
| github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= | ||||
| github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= | ||||
| github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= | ||||
| github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= | ||||
| github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= | ||||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||
| github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= | ||||
| github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= | ||||
| github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||||
| github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= | ||||
| github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | ||||
| github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= | ||||
| github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= | ||||
| github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= | ||||
| github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= | ||||
| github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= | ||||
| github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c= | ||||
| github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w= | ||||
| github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= | ||||
| github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= | ||||
| github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= | ||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= | ||||
| github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | ||||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||
| github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= | ||||
| github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= | ||||
| github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= | ||||
| github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= | ||||
| github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= | ||||
| go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= | ||||
| go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= | ||||
| golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= | ||||
| golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= | ||||
| golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||
| golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= | ||||
| golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= | ||||
| lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= | ||||
| lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= | ||||
| modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= | ||||
| modernc.org/cc/v3 v3.36.3 h1:uISP3F66UlixxWEcKuIWERa4TwrZENHSL8tWxZz8bHg= | ||||
| modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= | ||||
| modernc.org/ccgo/v3 v3.16.9 h1:AXquSwg7GuMk11pIdw7fmO1Y/ybgazVkMhsZWCV0mHM= | ||||
| modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= | ||||
| modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= | ||||
| modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= | ||||
| modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= | ||||
| modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= | ||||
| modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= | ||||
| modernc.org/libc v1.17.1 h1:Q8/Cpi36V/QBfuQaFVeisEBs3WqoGAJprZzmf7TfEYI= | ||||
| modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= | ||||
| modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= | ||||
| modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= | ||||
| modernc.org/memory v1.2.1 h1:dkRh86wgmq/bJu2cAS2oqBCz/KsMZU7TUM4CibQ7eBs= | ||||
| modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= | ||||
| modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= | ||||
| modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= | ||||
| modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= | ||||
| modernc.org/sqlite v1.18.1 h1:ko32eKt3jf7eqIkCgPAeHMBXw3riNSLhl2f3loEF7o8= | ||||
| modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= | ||||
| modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= | ||||
| modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= | ||||
| modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= | ||||
| modernc.org/tcl v1.13.1 h1:npxzTwFTZYM8ghWicVIX1cRWzj7Nd8i6AqqX2p+IYao= | ||||
| modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= | ||||
| modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= | ||||
| modernc.org/z v1.5.1 h1:RTNHdsrOpeoSeOF4FbzTo8gBYByaJ5xT7NgZ9ZqRiJM= | ||||
							
								
								
									
										33
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/cmd/users" | ||||
| 	"codeberg.org/u1f320/filer/cmd/web" | ||||
| 	"github.com/rs/zerolog" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| 	"github.com/rs/zerolog/pkgerrors" | ||||
| 	"github.com/urfave/cli/v2" | ||||
| ) | ||||
| 
 | ||||
| var app = &cli.App{ | ||||
| 	HelpName: "filer", | ||||
| 	Usage:    "Simple Go file server", | ||||
| 	Commands: []*cli.Command{ | ||||
| 		web.Command, | ||||
| 		users.Command, | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	// set the logger to output to console format | ||||
| 	// while performance is nice, the logger is not the place we'll ever get bottlenecked | ||||
| 	// and the console output is friendlier | ||||
| 	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "[15:04:05]"}) | ||||
| 	zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack | ||||
| 
 | ||||
| 	if err := app.Run(os.Args); err != nil { | ||||
| 		log.Error().Stack().Err(err).Msg("running app") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										12
									
								
								sqlc.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								sqlc.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| version: 2 | ||||
| sql: | ||||
|   - engine: "sqlite" | ||||
|     schema: "db/schema.sql" | ||||
|     queries: "db/queries.sql" | ||||
|     gen: | ||||
|       go: | ||||
|         package: "queries" | ||||
|         out: "db/queries" | ||||
|         overrides: | ||||
|           - column: "files.id" | ||||
|             go_type: "github.com/google/uuid.UUID" | ||||
							
								
								
									
										126
									
								
								store/local/local.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								store/local/local.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,126 @@ | |||
| package local | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"io" | ||||
| 	"io/fs" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/store" | ||||
| 	"emperror.dev/errors" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	StoreKey         = "local" | ||||
| 	ErrNotDirectory  = errors.Sentinel("not a directory") | ||||
| 	ErrAlreadyExists = errors.Sentinel("file already exists") | ||||
| 
 | ||||
| 	defaultPath = "uploads" | ||||
| ) | ||||
| 
 | ||||
| type localStore struct { | ||||
| 	path string | ||||
| } | ||||
| 
 | ||||
| var _ store.Store = (*localStore)(nil) | ||||
| 
 | ||||
| // New creates a new local store. | ||||
| // It creates the directory if it doesn't exist, | ||||
| func New(path string) (store.Store, error) { | ||||
| 	if path == "" { | ||||
| 		path = defaultPath | ||||
| 	} | ||||
| 
 | ||||
| 	if !strings.HasPrefix(path, "/") { | ||||
| 		wd, err := os.Getwd() | ||||
| 		if err != nil { | ||||
| 			return nil, errors.Wrap(err, "getting working directory") | ||||
| 		} | ||||
| 
 | ||||
| 		path = filepath.Join(wd, path) | ||||
| 	} | ||||
| 
 | ||||
| 	log.Info().Str("path", path).Msg("Using local storage") | ||||
| 
 | ||||
| 	fi, err := os.Stat(path) | ||||
| 	if err != nil { | ||||
| 		if !errors.Is(err, fs.ErrNotExist) { | ||||
| 			return nil, errors.Wrap(err, "calling Stat on path") | ||||
| 		} | ||||
| 
 | ||||
| 		err = os.MkdirAll(path, 0o744) | ||||
| 		if err != nil { | ||||
| 			return nil, errors.Wrap(err, "creating uploads directory") | ||||
| 		} | ||||
| 	} else if !fi.IsDir() { | ||||
| 		return nil, ErrNotDirectory | ||||
| 	} | ||||
| 
 | ||||
| 	return &localStore{ | ||||
| 		path: path, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (l *localStore) WriteFile(id uuid.UUID, data io.Reader, contentType string) (err error) { | ||||
| 	path := filepath.Join(l.path, id.String()) | ||||
| 
 | ||||
| 	_, err = os.Stat(path) | ||||
| 	if err == nil { | ||||
| 		return ErrAlreadyExists | ||||
| 	} | ||||
| 
 | ||||
| 	file, err := os.Create(path) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "creating file") | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		err = errors.Append(err, file.Close()) | ||||
| 	}() | ||||
| 
 | ||||
| 	_, err = io.Copy(file, data) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "writing data to file") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (l *localStore) GetFile(id uuid.UUID) (r io.Reader, err error) { | ||||
| 	path := filepath.Join(l.path, id.String()) | ||||
| 
 | ||||
| 	file, err := os.Open(path) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, fs.ErrNotExist) { | ||||
| 			return nil, store.ErrNotExist | ||||
| 		} | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		err = errors.Append(err, file.Close()) | ||||
| 	}() | ||||
| 
 | ||||
| 	b := new(bytes.Buffer) | ||||
| 	_, err = io.Copy(b, file) | ||||
| 	if err != nil { | ||||
| 		return nil, errors.Wrap(err, "copying file to buffer") | ||||
| 	} | ||||
| 
 | ||||
| 	return b, nil | ||||
| } | ||||
| 
 | ||||
| func (l *localStore) DeleteFile(id uuid.UUID) error { | ||||
| 	path := filepath.Join(l.path, id.String()) | ||||
| 
 | ||||
| 	err := os.Remove(path) | ||||
| 	if err != nil { | ||||
| 		if !errors.Is(err, fs.ErrNotExist) { | ||||
| 			return nil // a file already not existing is not an error | ||||
| 		} | ||||
| 
 | ||||
| 		return errors.Wrap(err, "removing file") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										19
									
								
								store/store.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								store/store.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | |||
| package store | ||||
| 
 | ||||
| import ( | ||||
| 	"io" | ||||
| 
 | ||||
| 	"emperror.dev/errors" | ||||
| 	"github.com/google/uuid" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	ErrNotExist        = errors.Sentinel("file does not exist") | ||||
| 	ErrInvalidStoreKey = errors.Sentinel("invalid $STORAGE key") | ||||
| ) | ||||
| 
 | ||||
| type Store interface { | ||||
| 	WriteFile(id uuid.UUID, data io.Reader, contentType string) error | ||||
| 	GetFile(id uuid.UUID) (io.Reader, error) | ||||
| 	DeleteFile(id uuid.UUID) error | ||||
| } | ||||
							
								
								
									
										48
									
								
								web/logger.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								web/logger.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| package web | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"runtime/debug" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/go-chi/chi/v5/middleware" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
| 
 | ||||
| func requestLogger(next http.Handler) http.Handler { | ||||
| 	fn := func(w http.ResponseWriter, r *http.Request) { | ||||
| 		ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) | ||||
| 
 | ||||
| 		t1 := time.Now() | ||||
| 		defer func() { | ||||
| 			t2 := time.Now() | ||||
| 
 | ||||
| 			// Recover and record stack traces in case of a panic | ||||
| 			if rec := recover(); rec != nil { | ||||
| 				log.Error(). | ||||
| 					Str("type", "error"). | ||||
| 					Timestamp(). | ||||
| 					Interface("recover_info", rec). | ||||
| 					Bytes("debug_stack", debug.Stack()). | ||||
| 					Msg("Handler panicked") | ||||
| 				http.Error(ww, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) | ||||
| 			} | ||||
| 
 | ||||
| 			// log end request | ||||
| 			log.Info(). | ||||
| 				Timestamp(). | ||||
| 				Str("remote_ip", r.RemoteAddr). | ||||
| 				Str("url", r.URL.Path). | ||||
| 				Str("proto", r.Proto). | ||||
| 				Str("method", r.Method). | ||||
| 				Int("status", ww.Status()). | ||||
| 				Dur("elapsed", t2.Sub(t1)). | ||||
| 				Int64("bytes_in", r.ContentLength). | ||||
| 				Int("bytes_out", ww.BytesWritten()). | ||||
| 				Msg(r.Method + " " + r.URL.Path) | ||||
| 		}() | ||||
| 
 | ||||
| 		next.ServeHTTP(ww, r) | ||||
| 	} | ||||
| 	return http.HandlerFunc(fn) | ||||
| } | ||||
							
								
								
									
										49
									
								
								web/show_file.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								web/show_file.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,49 @@ | |||
| package web | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/db/queries" | ||||
| 	"emperror.dev/errors" | ||||
| 	"github.com/go-chi/chi/v5" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
| 
 | ||||
| func (app *App) showFile(w http.ResponseWriter, r *http.Request) { | ||||
| 	ctx := r.Context() | ||||
| 	hash := chi.URLParam(r, "hash") | ||||
| 	filename := chi.URLParam(r, "filename") | ||||
| 
 | ||||
| 	file, err := app.db.GetFileByName(ctx, queries.GetFileByNameParams{ | ||||
| 		Filename: filename, | ||||
| 		Hash:     hash, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, sql.ErrNoRows) { | ||||
| 			http.Error(w, "File not found", http.StatusNotFound) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		log.Err(err).Str("hash", hash).Str("name", filename).Msg("getting file from database") | ||||
| 		http.Error(w, "Internal server error", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	data, err := app.db.Store.GetFile(file.ID) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Str("hash", hash).Str("name", filename).Msg("getting file data") | ||||
| 		http.Error(w, "Internal server error", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	w.Header().Add("Content-Type", file.ContentType) | ||||
| 	w.Header().Add("Content-Length", strconv.FormatInt(file.Size, 10)) | ||||
| 	_, err = io.Copy(w, data) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Str("hash", hash).Str("name", filename).Msg("writing file data") | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										103
									
								
								web/upload_file.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								web/upload_file.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | |||
| package web | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/sha256" | ||||
| 	"database/sql" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/db/queries" | ||||
| 	"emperror.dev/errors" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
| 
 | ||||
| // 500 megabytes | ||||
| const maxMemory = 500_000_000 | ||||
| 
 | ||||
| func (app *App) uploadFile(w http.ResponseWriter, r *http.Request) { | ||||
| 	ctx := r.Context() | ||||
| 
 | ||||
| 	token := r.Header.Get("Authorization") | ||||
| 	if token == "" { | ||||
| 		http.Error(w, "No token provided", http.StatusUnauthorized) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	u, err := app.db.GetUserByToken(ctx, token) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, sql.ErrNoRows) { | ||||
| 			http.Error(w, "Invalid token provided", http.StatusForbidden) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	err = r.ParseMultipartForm(maxMemory) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Msg("parsing multipart form data") | ||||
| 		http.Error(w, "Bad request", http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 	file, fileHeader, err := r.FormFile("file") | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Msg("getting file from multipart form") | ||||
| 		http.Error(w, "Bad request", http.StatusBadRequest) | ||||
| 	} | ||||
| 
 | ||||
| 	buffer := new(bytes.Buffer) | ||||
| 	_, err = io.Copy(buffer, file) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Msg("copying file to temporary buffer") | ||||
| 		http.Error(w, "Internal server error", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	contentType := http.DetectContentType(buffer.Bytes()) | ||||
| 	hash, err := getSha256Hash(buffer.Bytes()) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Msg("generating file hash") | ||||
| 		http.Error(w, "Internal server error", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	dbfile, err := app.db.CreateFile(ctx, queries.CreateFileParams{ | ||||
| 		ID:          uuid.New(), | ||||
| 		UserID:      u.ID, | ||||
| 		Filename:    fileHeader.Filename, | ||||
| 		ContentType: contentType, | ||||
| 		Hash:        hash, | ||||
| 		Size:        int64(buffer.Len()), | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Msg("creating file in database") | ||||
| 		http.Error(w, "Internal server error", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	err = app.db.Store.WriteFile(dbfile.ID, buffer, contentType) | ||||
| 	if err != nil { | ||||
| 		log.Err(err).Msg("creating file in store") | ||||
| 		err = app.db.DeleteFile(ctx, dbfile.ID) | ||||
| 		if err != nil { | ||||
| 			log.Err(err).Msg("deleting file from database") | ||||
| 		} | ||||
| 
 | ||||
| 		http.Error(w, "Internal server error", http.StatusInternalServerError) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	w.WriteHeader(http.StatusOK) | ||||
| 	w.Write([]byte(fmt.Sprintf("/%s/%s", hash, fileHeader.Filename))) | ||||
| } | ||||
| 
 | ||||
| func getSha256Hash(b []byte) (string, error) { | ||||
| 	hasher := sha256.New() | ||||
| 	_, err := hasher.Write(b) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return hex.EncodeToString(hasher.Sum(nil)), nil | ||||
| } | ||||
							
								
								
									
										63
									
								
								web/web.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								web/web.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,63 @@ | |||
| package web | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"regexp" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/filer/db" | ||||
| 	"github.com/go-chi/chi/v5" | ||||
| 	"github.com/go-chi/chi/v5/middleware" | ||||
| 	"github.com/go-chi/cors" | ||||
| 	"github.com/rs/zerolog/log" | ||||
| ) | ||||
| 
 | ||||
| type App struct { | ||||
| 	chi.Router | ||||
| 
 | ||||
| 	db *db.DB | ||||
| } | ||||
| 
 | ||||
| func New(db *db.DB) *App { | ||||
| 	r := chi.NewMux() | ||||
| 	r.Use(middleware.Recoverer) | ||||
| 	r.Use(requestLogger) | ||||
| 	r.Use(cors.Handler(cors.Options{ | ||||
| 		AllowedOrigins: []string{"https://*", "http://*"}, | ||||
| 		AllowedMethods: []string{"GET", "OPTIONS"}, | ||||
| 		MaxAge:         300, | ||||
| 	})) | ||||
| 
 | ||||
| 	app := &App{ | ||||
| 		Router: r, | ||||
| 		db:     db, | ||||
| 	} | ||||
| 
 | ||||
| 	app.Get("/{hash}/{filename}", app.showFile) | ||||
| 	app.Post("/upload", app.uploadFile) | ||||
| 
 | ||||
| 	return app | ||||
| } | ||||
| 
 | ||||
| var portRegex = regexp.MustCompile(`^\d{2,}$`) | ||||
| 
 | ||||
| func (app *App) Run(ctx context.Context) { | ||||
| 	port := os.Getenv("PORT") | ||||
| 	if !portRegex.MatchString(port) { | ||||
| 		log.Error().Str("port", port).Msg("$PORT is not a valid integer") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	listenAddr := ":" + port | ||||
| 
 | ||||
| 	ech := make(chan error) | ||||
| 	go func() { | ||||
| 		ech <- http.ListenAndServe(listenAddr, app) | ||||
| 	}() | ||||
| 
 | ||||
| 	log.Info().Str("port", port).Msg("Running server") | ||||
| 	if err := <-ech; err != nil { | ||||
| 		log.Err(err).Msg("Running server") | ||||
| 	} | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue