Compare commits
	
		
			No commits in common. "3a7d82a396fbbf9ce3efdaf97c9ecf2edea49011" and "ff92e302324a5e3bccb3d626bf8892c559cf2dff" have entirely different histories.
		
	
	
		
			
				3a7d82a396
			
			...
			
				ff92e30232
			
		
	
		
					 9 changed files with 356 additions and 517 deletions
				
			
		
							
								
								
									
										45
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										45
									
								
								Makefile
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -16,30 +16,41 @@ build:
 | 
			
		|||
 | 
			
		||||
build_linux_amd64:
 | 
			
		||||
	cd src \
 | 
			
		||||
	&& GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-linux-amd64 cmd/collerd/main.go \
 | 
			
		||||
	&& GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-linux-amd64 cmd/coller/main.go \
 | 
			
		||||
	&& GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-linux-amd64 cmd/copier/main.go
 | 
			
		||||
	&& GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-${APPVERSION}-linux-amd64 cmd/collerd/main.go \
 | 
			
		||||
	&& GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-${APPVERSION}-linux-amd64 cmd/coller/main.go \
 | 
			
		||||
	&& GOOS=linux GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-${APPVERSION}-linux-amd64 cmd/copier/main.go
 | 
			
		||||
 | 
			
		||||
archive_linux_amd64:
 | 
			
		||||
	mkdir -p releases/coller-${APPVERSION}-linux-amd64 \
 | 
			
		||||
	&& cp bin/collerd-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/collerd \
 | 
			
		||||
	&& cp bin/coller-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/coller \
 | 
			
		||||
	&& cp bin/copier-${APPVERSION}-linux-amd64 releases/coller-${APPVERSION}-linux-amd64/copier \
 | 
			
		||||
	&& cd releases/ \
 | 
			
		||||
	&& tar cvpzf coller-${APPVERSION}-linux-amd64.tar.gz coller-${APPVERSION}-linux-amd64
 | 
			
		||||
 | 
			
		||||
build_darwin_arm64:
 | 
			
		||||
	cd src \
 | 
			
		||||
	&& GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-darwin-arm64 cmd/collerd/main.go \
 | 
			
		||||
	&& GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-darwin-arm64 cmd/coller/main.go \
 | 
			
		||||
	&& GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-darwin-arm64 cmd/copier/main.go
 | 
			
		||||
	&& GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/collerd-${APPVERSION}-darwin-arm64 cmd/collerd/main.go \
 | 
			
		||||
	&& GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/coller-${APPVERSION}-darwin-arm64 cmd/coller/main.go \
 | 
			
		||||
	&& GOOS=darwin GOARCH=arm64 go build -ldflags "${LDFLAGS}" -o ../bin/copier-${APPVERSION}-darwin-arm64 cmd/copier/main.go
 | 
			
		||||
 | 
			
		||||
create_release:
 | 
			
		||||
	mkdir -p releases/${APPVERSION} \
 | 
			
		||||
	&& cp -p bin/collerd-linux-amd64 releases/${APPVERSION}/collerd-linux-amd64 \
 | 
			
		||||
	&& cp -p bin/coller-linux-amd64 releases/${APPVERSION}/coller-linux-amd64 \
 | 
			
		||||
	&& cp -p bin/copier-linux-amd64 releases/${APPVERSION}/copier-linux-amd64 \
 | 
			
		||||
	&& cp -p bin/collerd-darwin-arm64 releases/${APPVERSION}/collerd-darwin-arm64 \
 | 
			
		||||
	&& cp -p bin/coller-darwin-arm64 releases/${APPVERSION}/coller-darwin-arm64 \
 | 
			
		||||
	&& cp -p bin/copier-darwin-arm64 releases/${APPVERSION}/copier-darwin-arm64
 | 
			
		||||
archive_darwin_arm64:
 | 
			
		||||
	mkdir -p releases/coller-${APPVERSION}-darwin-arm64 \
 | 
			
		||||
	&& cp bin/collerd-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/collerd \
 | 
			
		||||
	&& cp bin/coller-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/coller \
 | 
			
		||||
	&& cp bin/copier-${APPVERSION}-darwin-arm64 releases/coller-${APPVERSION}-darwin-arm64/copier \
 | 
			
		||||
	&& cd releases/ \
 | 
			
		||||
	&& tar cvpzf coller-${APPVERSION}-darwin-arm64.tar.gz coller-${APPVERSION}-darwin-arm64
 | 
			
		||||
 | 
			
		||||
checksum:
 | 
			
		||||
	cd releases/${APPVERSION} \
 | 
			
		||||
	&& sha256sum collerd-* coller-* copier-* > checksums.txt
 | 
			
		||||
	cd releases \
 | 
			
		||||
	&& sha256sum *.tar.gz > checksums.txt
 | 
			
		||||
 | 
			
		||||
releases: build_linux_amd64 build_darwin_arm64 create_release checksum
 | 
			
		||||
clean_for_releases:
 | 
			
		||||
	rm -rf releases/coller-${APPVERSION}-linux-amd64 \
 | 
			
		||||
	&& rm -rf releases/coller-${APPVERSION}-darwin-arm64
 | 
			
		||||
 | 
			
		||||
releases: build_linux_amd64 build_darwin_arm64 archive_linux_amd64 archive_darwin_arm64 checksum clean_for_releases
 | 
			
		||||
 | 
			
		||||
releases_with_docker:
 | 
			
		||||
	docker run -it -v $(shell pwd):/mnt -w /mnt -e "UID=$(shell id -u)" -e "GID=$(shell id -g)" ${DOCKER_IMAGE} ./docker/build.sh
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,4 +17,4 @@ apt-get install -y libx11-dev
 | 
			
		|||
 | 
			
		||||
make releases
 | 
			
		||||
 | 
			
		||||
chown ${UID}:${GID} -R bin releases
 | 
			
		||||
chown ${UID}:${GID} -R releases
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,12 +117,3 @@ func ToLowerStringSlice(src []string) (dst []string) {
 | 
			
		|||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func InSlice(s []string, elem string) bool {
 | 
			
		||||
	for _, v := range s {
 | 
			
		||||
		if v == elem {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,25 +85,3 @@ func TestToLowerStringsSlice(t *testing.T) {
 | 
			
		|||
		t.Logf("got '%s', want '%s'", got, expected)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInSlice(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		elem     string
 | 
			
		||||
		s        []string
 | 
			
		||||
		expected bool
 | 
			
		||||
	}{
 | 
			
		||||
		{"linux", []string{"linux", "darwin"}, true},
 | 
			
		||||
		{"windows", []string{"linux", "darwin"}, false},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range tests {
 | 
			
		||||
		t.Run(fmt.Sprintf("TestInSlice#%s", tc.elem), func(t *testing.T) {
 | 
			
		||||
			got := InSlice(tc.s, tc.elem)
 | 
			
		||||
			if got != tc.expected {
 | 
			
		||||
				t.Errorf("got '%t', want '%t'", got, tc.expected)
 | 
			
		||||
			} else {
 | 
			
		||||
				t.Logf("got '%t', want '%t'", got, tc.expected)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,149 +0,0 @@
 | 
			
		|||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
 | 
			
		||||
	"git.riou.xyz/jriou/coller/internal"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	fmt.Fprintf(w, "OK")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNoteHandler struct {
 | 
			
		||||
	logger        *slog.Logger
 | 
			
		||||
	db            *Database
 | 
			
		||||
	maxUploadSize int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNotePayload struct {
 | 
			
		||||
	Content         string `json:"content"`
 | 
			
		||||
	Password        string `json:"password"`
 | 
			
		||||
	Encrypted       bool   `json:"encrypted"`
 | 
			
		||||
	Expiration      int    `json:"expiration"`
 | 
			
		||||
	DeleteAfterRead bool   `json:"delete_after_read"`
 | 
			
		||||
	Language        string `json:"language"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNoteResponse struct {
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
 | 
			
		||||
	bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize)
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
 | 
			
		||||
	var body CreateNotePayload
 | 
			
		||||
	err := json.NewDecoder(bodyReader).Decode(&body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not decode payload to create note", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, err := internal.Decode(body.Content)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not decode content", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Create(content, body.Password, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not create note", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(http.StatusOK)
 | 
			
		||||
	json.NewEncoder(w).Encode(CreateNoteResponse{ID: note.ID})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetNoteHandler struct {
 | 
			
		||||
	logger *slog.Logger
 | 
			
		||||
	db     *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
			
		||||
 | 
			
		||||
	id := mux.Vars(r)["id"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not get note", err)
 | 
			
		||||
	} else if note == nil {
 | 
			
		||||
		w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
	} else {
 | 
			
		||||
		if note.Encrypted {
 | 
			
		||||
			w.Header().Set("Content-Type", "application/octet-stream")
 | 
			
		||||
		}
 | 
			
		||||
		w.WriteHeader(http.StatusOK)
 | 
			
		||||
		fmt.Fprint(w, string(note.Content))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetProtectedNoteHandler struct {
 | 
			
		||||
	logger *slog.Logger
 | 
			
		||||
	db     *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
			
		||||
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	id := vars["id"]
 | 
			
		||||
	password := vars["password"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not get note", err)
 | 
			
		||||
		return
 | 
			
		||||
	} else if note == nil {
 | 
			
		||||
		w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if password != "" && note.Encrypted {
 | 
			
		||||
		note.Content, err = internal.Decrypt(note.Content, password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			WriteError(w, "could not decrypt note", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(http.StatusOK)
 | 
			
		||||
	fmt.Fprint(w, string(note.Content))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ClientHandler struct {
 | 
			
		||||
	logger  *slog.Logger
 | 
			
		||||
	version string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ClientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.logger.Debug("rendering client redirection")
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	os := vars["os"]
 | 
			
		||||
	arch := vars["arch"]
 | 
			
		||||
	clientName := vars["clientName"]
 | 
			
		||||
 | 
			
		||||
	if !internal.InSlice(supportedOSes, os) || !internal.InSlice(supportedArches, arch) || !internal.InSlice(supportedClients, clientName) {
 | 
			
		||||
		w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	version := h.version
 | 
			
		||||
	if version == "" {
 | 
			
		||||
		version = "latest"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	http.Redirect(w, r, fmt.Sprintf("https://git.riou.xyz/jriou/%s/releases/download/%s/%s-%s-%s", clientName, version, clientName, os, arch), http.StatusMovedPermanently)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,234 +0,0 @@
 | 
			
		|||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
 | 
			
		||||
	"git.riou.xyz/jriou/coller/internal"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PageData struct {
 | 
			
		||||
	Title                  string
 | 
			
		||||
	Version                string
 | 
			
		||||
	Expirations            []int
 | 
			
		||||
	Expiration             int
 | 
			
		||||
	Languages              []string
 | 
			
		||||
	Err                    error
 | 
			
		||||
	URL                    string
 | 
			
		||||
	Note                   *Note
 | 
			
		||||
	EnableUploadFileButton bool
 | 
			
		||||
	BootstrapDirectory     string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HomeHandler struct {
 | 
			
		||||
	Templates *template.Template
 | 
			
		||||
	PageData  PageData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "index", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNoteWithFormHandler struct {
 | 
			
		||||
	Templates     *template.Template
 | 
			
		||||
	PageData      PageData
 | 
			
		||||
	logger        *slog.Logger
 | 
			
		||||
	db            *Database
 | 
			
		||||
	maxUploadSize int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.PageData.Err = nil
 | 
			
		||||
	templateName := "create"
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("parsing multipart form")
 | 
			
		||||
	err := r.ParseMultipartForm(h.maxUploadSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = err
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("parsing content")
 | 
			
		||||
	content := []byte(r.FormValue("content"))
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("parsing file")
 | 
			
		||||
	file, handler, err := r.FormFile("file")
 | 
			
		||||
	if err != nil && !errors.Is(err, http.ErrMissingFile) {
 | 
			
		||||
		h.PageData.Err = err
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !errors.Is(err, http.ErrMissingFile) {
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("checking file size")
 | 
			
		||||
		if handler.Size > h.maxUploadSize {
 | 
			
		||||
			h.PageData.Err = fmt.Errorf("file too large (%d > %d)", handler.Size, h.maxUploadSize)
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("checking file content type")
 | 
			
		||||
		if !strings.HasPrefix(handler.Header.Get("Content-Type"), "text/") {
 | 
			
		||||
			h.PageData.Err = fmt.Errorf("text file expected (got %s)", handler.Header.Get("Content-Type"))
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("reading uploaded file")
 | 
			
		||||
		var fileContent bytes.Buffer
 | 
			
		||||
		n, err := io.Copy(&fileContent, file)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			h.PageData.Err = err
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("file uploaded", slog.Any("bytes", n))
 | 
			
		||||
		if n != 0 {
 | 
			
		||||
			content = fileContent.Bytes()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("checking content")
 | 
			
		||||
	if content == nil || len(content) == 0 {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("empty note")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("checking inputs")
 | 
			
		||||
	noPassword := r.FormValue("no-password")
 | 
			
		||||
	password := r.FormValue("password")
 | 
			
		||||
	expiration := r.FormValue("expiration")
 | 
			
		||||
	deleteAfterRead := r.FormValue("delete-after-read")
 | 
			
		||||
	language := r.FormValue("language")
 | 
			
		||||
 | 
			
		||||
	if password == "" && noPassword == "" {
 | 
			
		||||
		h.logger.Debug("generating password")
 | 
			
		||||
		password = internal.GenerateChars(passwordLength)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("computing expiration")
 | 
			
		||||
	var expirationInt int
 | 
			
		||||
	if expiration == "Expiration" {
 | 
			
		||||
		expirationInt = 0
 | 
			
		||||
	} else {
 | 
			
		||||
		expirationInt, _ = strconv.Atoi(expiration)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("saving note to the database")
 | 
			
		||||
	note, err := h.db.Create(content, password, password != "", expirationInt, deleteAfterRead != "", language)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = err
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("building note url")
 | 
			
		||||
	var scheme = "http://"
 | 
			
		||||
	if r.TLS != nil {
 | 
			
		||||
		scheme = "https://"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID)
 | 
			
		||||
	if password != "" {
 | 
			
		||||
		h.PageData.URL += "/" + password
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("rendering page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "create", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetWebNoteHandler struct {
 | 
			
		||||
	Templates *template.Template
 | 
			
		||||
	PageData  PageData
 | 
			
		||||
	logger    *slog.Logger
 | 
			
		||||
	db        *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.PageData.Err = nil
 | 
			
		||||
	templateName := "note"
 | 
			
		||||
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	id := vars["id"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("could not find note: %v", err)
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if note == nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if note.Encrypted {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("note is encrypted")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.PageData.Note = note
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("rendering note web page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "note", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.PageData.Err = nil
 | 
			
		||||
	templateName := "note"
 | 
			
		||||
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	id := vars["id"]
 | 
			
		||||
	password := vars["password"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("could not find note: %v", err)
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if note == nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if password != "" && note.Encrypted {
 | 
			
		||||
		note.Content, err = internal.Decrypt(note.Content, password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err)
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.PageData.Note = note
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("rendering protected note web page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "note", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *ClientsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.logger.Debug("rendering clients web page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "clients", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,16 @@
 | 
			
		|||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
| 
						 | 
				
			
			@ -15,12 +19,7 @@ import (
 | 
			
		|||
	"git.riou.xyz/jriou/coller/internal"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	passwordLength   = internal.MIN_PASSWORD_LENGTH
 | 
			
		||||
	supportedOSes    = []string{"linux", "darwin"}
 | 
			
		||||
	supportedArches  = []string{"amd64", "arm64"}
 | 
			
		||||
	supportedClients = []string{"coller", "copier"}
 | 
			
		||||
)
 | 
			
		||||
var passwordLength = internal.MIN_PASSWORD_LENGTH
 | 
			
		||||
 | 
			
		||||
type Server struct {
 | 
			
		||||
	logger  *slog.Logger
 | 
			
		||||
| 
						 | 
				
			
			@ -75,6 +74,293 @@ func WriteError(w http.ResponseWriter, message string, err error) {
 | 
			
		|||
	}.ToJSON())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	fmt.Fprintf(w, "OK")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNoteHandler struct {
 | 
			
		||||
	logger        *slog.Logger
 | 
			
		||||
	db            *Database
 | 
			
		||||
	maxUploadSize int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNotePayload struct {
 | 
			
		||||
	Content         string `json:"content"`
 | 
			
		||||
	Password        string `json:"password"`
 | 
			
		||||
	Encrypted       bool   `json:"encrypted"`
 | 
			
		||||
	Expiration      int    `json:"expiration"`
 | 
			
		||||
	DeleteAfterRead bool   `json:"delete_after_read"`
 | 
			
		||||
	Language        string `json:"language"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNoteResponse struct {
 | 
			
		||||
	ID string `json:"id"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "application/json")
 | 
			
		||||
 | 
			
		||||
	bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize)
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
 | 
			
		||||
	var body CreateNotePayload
 | 
			
		||||
	err := json.NewDecoder(bodyReader).Decode(&body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not decode payload to create note", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, err := internal.Decode(body.Content)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not decode content", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Create(content, body.Password, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not create note", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(http.StatusOK)
 | 
			
		||||
	json.NewEncoder(w).Encode(CreateNoteResponse{ID: note.ID})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetNoteHandler struct {
 | 
			
		||||
	logger *slog.Logger
 | 
			
		||||
	db     *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
			
		||||
 | 
			
		||||
	id := mux.Vars(r)["id"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not get note", err)
 | 
			
		||||
	} else if note == nil {
 | 
			
		||||
		w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
	} else {
 | 
			
		||||
		if note.Encrypted {
 | 
			
		||||
			w.Header().Set("Content-Type", "application/octet-stream")
 | 
			
		||||
		}
 | 
			
		||||
		w.WriteHeader(http.StatusOK)
 | 
			
		||||
		fmt.Fprint(w, string(note.Content))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetProtectedNoteHandler struct {
 | 
			
		||||
	logger *slog.Logger
 | 
			
		||||
	db     *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
 | 
			
		||||
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	id := vars["id"]
 | 
			
		||||
	password := vars["password"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		WriteError(w, "could not get note", err)
 | 
			
		||||
		return
 | 
			
		||||
	} else if note == nil {
 | 
			
		||||
		w.WriteHeader(http.StatusNotFound)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if password != "" && note.Encrypted {
 | 
			
		||||
		note.Content, err = internal.Decrypt(note.Content, password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			WriteError(w, "could not decrypt note", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(http.StatusOK)
 | 
			
		||||
	fmt.Fprint(w, string(note.Content))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PageData struct {
 | 
			
		||||
	Title                  string
 | 
			
		||||
	Version                string
 | 
			
		||||
	Expirations            []int
 | 
			
		||||
	Expiration             int
 | 
			
		||||
	Languages              []string
 | 
			
		||||
	Err                    error
 | 
			
		||||
	URL                    string
 | 
			
		||||
	Note                   *Note
 | 
			
		||||
	EnableUploadFileButton bool
 | 
			
		||||
	BootstrapDirectory     string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type HomeHandler struct {
 | 
			
		||||
	Templates *template.Template
 | 
			
		||||
	PageData  PageData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "index", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CreateNoteWithFormHandler struct {
 | 
			
		||||
	Templates     *template.Template
 | 
			
		||||
	PageData      PageData
 | 
			
		||||
	logger        *slog.Logger
 | 
			
		||||
	db            *Database
 | 
			
		||||
	maxUploadSize int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.PageData.Err = nil
 | 
			
		||||
	templateName := "create"
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("parsing multipart form")
 | 
			
		||||
	err := r.ParseMultipartForm(h.maxUploadSize)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = err
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("parsing content")
 | 
			
		||||
	content := []byte(r.FormValue("content"))
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("parsing file")
 | 
			
		||||
	file, handler, err := r.FormFile("file")
 | 
			
		||||
	if err != nil && !errors.Is(err, http.ErrMissingFile) {
 | 
			
		||||
		h.PageData.Err = err
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !errors.Is(err, http.ErrMissingFile) {
 | 
			
		||||
		defer file.Close()
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("checking file size")
 | 
			
		||||
		if handler.Size > h.maxUploadSize {
 | 
			
		||||
			h.PageData.Err = fmt.Errorf("file too large (%d > %d)", handler.Size, h.maxUploadSize)
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("checking file content type")
 | 
			
		||||
		if !strings.HasPrefix(handler.Header.Get("Content-Type"), "text/") {
 | 
			
		||||
			h.PageData.Err = fmt.Errorf("text file expected (got %s)", handler.Header.Get("Content-Type"))
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("reading uploaded file")
 | 
			
		||||
		var fileContent bytes.Buffer
 | 
			
		||||
		n, err := io.Copy(&fileContent, file)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			h.PageData.Err = err
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		h.logger.Debug("file uploaded", slog.Any("bytes", n))
 | 
			
		||||
		if n != 0 {
 | 
			
		||||
			content = fileContent.Bytes()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("checking content")
 | 
			
		||||
	if content == nil || len(content) == 0 {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("empty note")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("checking inputs")
 | 
			
		||||
	noPassword := r.FormValue("no-password")
 | 
			
		||||
	password := r.FormValue("password")
 | 
			
		||||
	expiration := r.FormValue("expiration")
 | 
			
		||||
	deleteAfterRead := r.FormValue("delete-after-read")
 | 
			
		||||
	language := r.FormValue("language")
 | 
			
		||||
 | 
			
		||||
	if password == "" && noPassword == "" {
 | 
			
		||||
		h.logger.Debug("generating password")
 | 
			
		||||
		password = internal.GenerateChars(passwordLength)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("computing expiration")
 | 
			
		||||
	var expirationInt int
 | 
			
		||||
	if expiration == "Expiration" {
 | 
			
		||||
		expirationInt = 0
 | 
			
		||||
	} else {
 | 
			
		||||
		expirationInt, _ = strconv.Atoi(expiration)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("saving note to the database")
 | 
			
		||||
	note, err := h.db.Create(content, password, password != "", expirationInt, deleteAfterRead != "", language)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = err
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("building note url")
 | 
			
		||||
	var scheme = "http://"
 | 
			
		||||
	if r.TLS != nil {
 | 
			
		||||
		scheme = "https://"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID)
 | 
			
		||||
	if password != "" {
 | 
			
		||||
		h.PageData.URL += "/" + password
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("rendering page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "create", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetWebNoteHandler struct {
 | 
			
		||||
	Templates *template.Template
 | 
			
		||||
	PageData  PageData
 | 
			
		||||
	logger    *slog.Logger
 | 
			
		||||
	db        *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.PageData.Err = nil
 | 
			
		||||
	templateName := "note"
 | 
			
		||||
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	id := vars["id"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("could not find note: %v", err)
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if note == nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if note.Encrypted {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("note is encrypted")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.PageData.Note = note
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("rendering note web page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "note", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetProtectedWebNoteHandler struct {
 | 
			
		||||
	Templates *template.Template
 | 
			
		||||
	PageData  PageData
 | 
			
		||||
| 
						 | 
				
			
			@ -82,10 +368,41 @@ type GetProtectedWebNoteHandler struct {
 | 
			
		|||
	db        *Database
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ClientsHandler struct {
 | 
			
		||||
	Templates *template.Template
 | 
			
		||||
	PageData  PageData
 | 
			
		||||
	logger    *slog.Logger
 | 
			
		||||
func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	h.PageData.Err = nil
 | 
			
		||||
	templateName := "note"
 | 
			
		||||
 | 
			
		||||
	vars := mux.Vars(r)
 | 
			
		||||
	id := vars["id"]
 | 
			
		||||
	password := vars["password"]
 | 
			
		||||
 | 
			
		||||
	note, err := h.db.Get(id)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("could not find note: %v", err)
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if note == nil {
 | 
			
		||||
		h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
 | 
			
		||||
		h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if password != "" && note.Encrypted {
 | 
			
		||||
		note.Content, err = internal.Decrypt(note.Content, password)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err)
 | 
			
		||||
			h.Templates.ExecuteTemplate(w, templateName, h.PageData)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h.PageData.Note = note
 | 
			
		||||
 | 
			
		||||
	h.logger.Debug("rendering protected note web page")
 | 
			
		||||
	h.Templates.ExecuteTemplate(w, "note", h.PageData)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//go:embed templates/*
 | 
			
		||||
| 
						 | 
				
			
			@ -139,14 +456,6 @@ func (s *Server) Start() error {
 | 
			
		|||
	}
 | 
			
		||||
	r.Path("/create").Handler(createNoteWithFormHandler).Methods("POST")
 | 
			
		||||
 | 
			
		||||
	clientsHandler := &ClientsHandler{
 | 
			
		||||
		Templates: templates,
 | 
			
		||||
		PageData:  p,
 | 
			
		||||
		logger:    s.logger,
 | 
			
		||||
	}
 | 
			
		||||
	r.Path("/clients.html").Handler(clientsHandler).Methods("GET")
 | 
			
		||||
	r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(&ClientHandler{logger: s.logger, version: p.Version}).Methods("GET")
 | 
			
		||||
 | 
			
		||||
	protectedWebNoteHandler := &GetProtectedWebNoteHandler{
 | 
			
		||||
		Templates: templates,
 | 
			
		||||
		PageData:  p,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,62 +0,0 @@
 | 
			
		|||
{{define "clients"}}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" data-bs-theme="light">
 | 
			
		||||
 | 
			
		||||
{{block "head" .}}{{end}}
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    {{block "header" .}}{{end}}
 | 
			
		||||
    <div class="container mb-4">
 | 
			
		||||
        <p class="fs-4">Command-line clients</p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="container mb-4">
 | 
			
		||||
 | 
			
		||||
        <table class="table">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="col">Name</th>
 | 
			
		||||
                    <th scope="col">Docs</th>
 | 
			
		||||
                    <th scope="col">OS</th>
 | 
			
		||||
                    <th scope="col">Arch</th>
 | 
			
		||||
                    <th scope="col">Download</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="row" rowspan="2">coller</th>
 | 
			
		||||
                    <td rowspan="2"><a
 | 
			
		||||
                            href="https://git.riou.xyz/jriou/coller/src/{{if .Version}}tag/{{.Version}}{{else}}branch/main{{end}}/src/cmd/coller/README.md">📄</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>Linux</td>
 | 
			
		||||
                    <td>x86-64</td>
 | 
			
		||||
                    <td><a href="/clients/linux-amd64/coller">💾</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>macOS</td>
 | 
			
		||||
                    <td>ARM64</td>
 | 
			
		||||
                    <td><a href="/clients/darwin-arm64/coller">💾</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <th scope="row" rowspan="2">copier</th>
 | 
			
		||||
                    <td rowspan="2"><a
 | 
			
		||||
                            href="https://git.riou.xyz/jriou/coller/src/{{if .Version}}tag/{{.Version}}{{else}}branch/main{{end}}/src/cmd/copier/README.md">📄</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>Linux</td>
 | 
			
		||||
                    <td>x86-64</td>
 | 
			
		||||
                    <td><a href="/clients/linux-amd64/copier">💾</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                    <td>macOS</td>
 | 
			
		||||
                    <td>ARM64</td>
 | 
			
		||||
                    <td><a href="/clients/darwin-arm64/copier">💾</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {{block "footer" .}}{{end}}
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
{{end}}
 | 
			
		||||
| 
						 | 
				
			
			@ -5,11 +5,6 @@
 | 
			
		|||
            <a class="d-flex mb-3 mb-md-0 me-md-auto text-dark text-decoration-none" id="titleHeader" href="/">
 | 
			
		||||
                <span class="fs-3">{{.Title}}</span>
 | 
			
		||||
            </a>
 | 
			
		||||
            <ul class="nav nav-pills align-items-center">
 | 
			
		||||
                <li class="nav-item px-2">
 | 
			
		||||
                    <a href="/clients.html">Command-line clients</a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
            <ul class="nav nav-pills align-items-center">
 | 
			
		||||
                <li class="nav-item">
 | 
			
		||||
                    <div class="form-check form-switch"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue