[mirror] Command-line application for uploading a site to a git-pages server

Implement the tool.

Changed files
+175 -2
+17 -2
README.md
··· 1 git-pages-cli 2 ============= 3 4 - _git-pages-cli_ is a command-line application for uploading sites to [git-pages]. It is not ready for use yet. 5 6 [git-pages]: https://codeberg.org/git-pages/git-pages 7 ··· 19 Usage 20 ----- 21 22 - TBD 23 24 25 26 License
··· 1 git-pages-cli 2 ============= 3 4 + _git-pages-cli_ is a command-line application for uploading sites to [git-pages]. 5 6 [git-pages]: https://codeberg.org/git-pages/git-pages 7 ··· 19 Usage 20 ----- 21 22 + To deploy a site from a git repository available on the internet (`--password` may be omitted if the repository is allowlisted via DNS): 23 + 24 + ```console 25 + $ git-pages-cli https://mycoolweb.site --password xyz --upload-git https://codeberg.org/username/mycoolweb.site.git 26 + ``` 27 + 28 + To deploy a site from a directory on your machine: 29 + 30 + ```console 31 + $ git-pages-cli https://mycoolweb.site --password xyz --upload-dir site-contents 32 + ``` 33 + 34 + To delete a site: 35 36 + ```console 37 + $ git-pages-cli https://mycoolweb.site --password xyz --delete 38 + ``` 39 40 41 License
+5
go.mod
··· 1 module codeberg.org/git-pages/git-pages-cli 2 3 go 1.25.0
··· 1 module codeberg.org/git-pages/git-pages-cli 2 3 go 1.25.0 4 + 5 + require ( 6 + github.com/spf13/pflag v1.0.10 7 + github.com/klauspost/compress v1.18.1 8 + )
+4
go.sum
···
··· 1 + github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= 2 + github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= 3 + github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 4 + github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+149
main.go
··· 1 package main 2 3 func main() { 4 }
··· 1 package main 2 3 + import ( 4 + "archive/tar" 5 + "bytes" 6 + "crypto/sha256" 7 + "fmt" 8 + "io" 9 + "io/fs" 10 + "net/http" 11 + "net/url" 12 + "os" 13 + 14 + "github.com/klauspost/compress/zstd" 15 + "github.com/spf13/pflag" 16 + ) 17 + 18 + var passwordFlag = pflag.String("password", "", "password for DNS challenge authorization") 19 + var challengeFlag = pflag.Bool("challenge", false, "compute DNS challenge entry from password") 20 + var uploadGitFlag = pflag.String("upload-git", "", "replace site with contents of specified git repository") 21 + var uploadDirFlag = pflag.String("upload-dir", "", "replace site with contents of specified directory") 22 + var deleteFlag = pflag.Bool("delete", false, "delete site") 23 + var verboseFlag = pflag.Bool("verbose", false, "display more information for debugging") 24 + 25 + func singleOperation() bool { 26 + operations := 0 27 + if *challengeFlag { 28 + operations++ 29 + } 30 + if *uploadDirFlag != "" { 31 + operations++ 32 + } 33 + if *uploadGitFlag != "" { 34 + operations++ 35 + } 36 + if *deleteFlag { 37 + operations++ 38 + } 39 + return operations == 1 40 + } 41 + 42 + func archiveFS(fs fs.FS) (result []byte, err error) { 43 + buffer := bytes.Buffer{} 44 + zstdWriter, _ := zstd.NewWriter(&buffer) 45 + tarWriter := tar.NewWriter(zstdWriter) 46 + err = tarWriter.AddFS(fs) 47 + if err != nil { 48 + return 49 + } 50 + err = tarWriter.Close() 51 + if err != nil { 52 + return 53 + } 54 + err = zstdWriter.Close() 55 + if err != nil { 56 + return 57 + } 58 + result = buffer.Bytes() 59 + return 60 + } 61 + 62 func main() { 63 + pflag.Parse() 64 + if singleOperation() || len(pflag.Args()) != 1 { 65 + fmt.Fprintf(os.Stderr, "Usage: %s [-upload-git url|-upload-dir path|-delete] <site-url>\n", os.Args[0]) 66 + os.Exit(125) 67 + } 68 + 69 + var err error 70 + siteUrl, err := url.Parse(pflag.Args()[0]) 71 + if err != nil { 72 + fmt.Fprintf(os.Stderr, "error: invalid site URL: %s\n", err) 73 + os.Exit(1) 74 + } 75 + 76 + var request *http.Request 77 + switch { 78 + case *challengeFlag: 79 + if *passwordFlag == "" { 80 + fmt.Fprintf(os.Stderr, "error: no password specified") 81 + os.Exit(1) 82 + } 83 + 84 + challenge := sha256.Sum256(fmt.Appendf(nil, "%s %s", siteUrl.Hostname(), *passwordFlag)) 85 + fmt.Fprintf(os.Stdout, "%s. 3600 IN TXT \"%x\"\n", siteUrl.Hostname(), challenge) 86 + os.Exit(0) 87 + 88 + case *uploadGitFlag != "": 89 + uploadGitUrl, err := url.Parse(*uploadGitFlag) 90 + if err != nil { 91 + fmt.Fprintf(os.Stderr, "error: invalid repository URL: %s\n", err) 92 + os.Exit(1) 93 + } 94 + 95 + requestBody := []byte(uploadGitUrl.String()) 96 + request, err = http.NewRequest("PUT", siteUrl.String(), bytes.NewReader(requestBody)) 97 + if err != nil { 98 + fmt.Fprintf(os.Stderr, "error: %s\n", err) 99 + os.Exit(1) 100 + } 101 + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 102 + 103 + case *uploadDirFlag != "": 104 + uploadDirFS, err := os.OpenRoot(*uploadDirFlag) 105 + if err != nil { 106 + fmt.Fprintf(os.Stderr, "error: invalid directory: %s\n", err) 107 + os.Exit(1) 108 + } 109 + 110 + requestBody, err := archiveFS(uploadDirFS.FS()) 111 + if err != nil { 112 + fmt.Fprintf(os.Stderr, "error: %s\n", err) 113 + os.Exit(1) 114 + } 115 + 116 + request, err = http.NewRequest("PUT", siteUrl.String(), bytes.NewReader(requestBody)) 117 + if err != nil { 118 + fmt.Fprintf(os.Stderr, "error: %s\n", err) 119 + os.Exit(1) 120 + } 121 + request.Header.Add("Content-Type", "application/x-tar+zstd") 122 + 123 + case *deleteFlag: 124 + request, err = http.NewRequest("DELETE", siteUrl.String(), bytes.NewReader([]byte{})) 125 + if err != nil { 126 + fmt.Fprintf(os.Stderr, "error: %s\n", err) 127 + os.Exit(1) 128 + } 129 + 130 + default: 131 + panic("no operation chosen") 132 + } 133 + if *passwordFlag != "" { 134 + request.Header.Add("Authorization", fmt.Sprintf("Pages %s", *passwordFlag)) 135 + } 136 + 137 + response, err := http.DefaultClient.Do(request) 138 + if err != nil { 139 + fmt.Fprintf(os.Stderr, "error: %s\n", err) 140 + os.Exit(1) 141 + } 142 + if *verboseFlag { 143 + fmt.Fprintf(os.Stderr, "server: %s\n", response.Header.Get("Server")) 144 + } 145 + if response.StatusCode == 200 { 146 + fmt.Fprintf(os.Stdout, "result: %s\n", response.Header.Get("Update-Result")) 147 + os.Exit(0) 148 + } else { 149 + fmt.Fprintf(os.Stderr, "result: error\n") 150 + io.Copy(os.Stderr, response.Body) 151 + os.Exit(1) 152 + } 153 }