Skip to content

Commit

Permalink
feat(robherley#97): implement list index and rename
Browse files Browse the repository at this point in the history
list index implemented, however with unhelpful names it would be
nice to see more pleasant name options. so ability to rename and
specify name has been added too
  • Loading branch information
surdaft committed Sep 12, 2023
1 parent a8dd0eb commit b02203b
Show file tree
Hide file tree
Showing 16 changed files with 615 additions and 134 deletions.
6 changes: 6 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type Config struct {
// https://github.com/robherley/snips.sh/issues/39
EnableGuesser bool `default:"True" desc:"enable guesslang model to detect file types"`

// Generally more geared towards self-hosted instances, or public instances
// with modifications to snips.sh.
EnableApi bool `default:"False" desc:"enable the snips.sh api"`

ListIndex bool `default:"False" desc:"enable index page showing recent snippets"`

HMACKey string `default:"hmac-and-cheese" desc:"symmetric key used to sign URLs"`

FileCompression bool `default:"True" desc:"enable compression of file contents"`
Expand Down
4 changes: 4 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ type DB interface {
CreateFile(ctx context.Context, file *snips.File, maxFiles uint64) error
// UpdateFile updates a file.
UpdateFile(ctx context.Context, file *snips.File) error
// RenameFile updates a file od.
RenameFile(ctx context.Context, file *snips.File, oldId string) error
// DeleteFile deletes a file by its ID.
DeleteFile(ctx context.Context, id string) error
// FindFiles find files, paginated
FindFiles(ctx context.Context, page int) ([]*snips.File, error)
// FindFilesByUser returns all files for a user. Does not include file content.
FindFilesByUser(ctx context.Context, userID string) ([]*snips.File, error)
// FindPublicKeyByFingerprint returns a public key by its fingerprint.
Expand Down
87 changes: 86 additions & 1 deletion internal/db/db_sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ func (s *Sqlite) CreateFile(ctx context.Context, file *snips.File, maxFileCount
return ErrFileLimit
}

file.ID = id.New()
if file.ID == "" {
file.ID = id.New()
}

file.CreatedAt = time.Now().UTC()
file.UpdatedAt = time.Now().UTC()

Expand Down Expand Up @@ -146,12 +149,43 @@ func (s *Sqlite) CreateFile(ctx context.Context, file *snips.File, maxFileCount
return nil
}

func (s *Sqlite) RenameFile(ctx context.Context, file *snips.File, oldId string) error {
file.UpdatedAt = time.Now().UTC()

const query = `
UPDATE files
SET
id = ?,
updated_at = ?,
size = ?,
content = ?,
private = ?,
type = ?
WHERE id = ?
`

if _, err := s.ExecContext(ctx, query,
file.ID,
file.UpdatedAt,
file.Size,
file.RawContent,
file.Private,
file.Type,
oldId,
); err != nil {
return err
}

return nil
}

func (s *Sqlite) UpdateFile(ctx context.Context, file *snips.File) error {
file.UpdatedAt = time.Now().UTC()

const query = `
UPDATE files
SET
id = ?,
updated_at = ?,
size = ?,
content = ?,
Expand All @@ -161,6 +195,7 @@ func (s *Sqlite) UpdateFile(ctx context.Context, file *snips.File) error {
`

if _, err := s.ExecContext(ctx, query,
file.ID,
file.UpdatedAt,
file.Size,
file.RawContent,
Expand All @@ -187,6 +222,56 @@ func (s *Sqlite) DeleteFile(ctx context.Context, id string) error {
return nil
}

func (s *Sqlite) FindFiles(ctx context.Context, page int) ([]*snips.File, error) {
const query = `
SELECT
id,
created_at,
updated_at,
size,
private,
type,
user_id
FROM files
WHERE private = 0
ORDER BY created_at DESC
LIMIT ?, 10
`

if page < 0 {
page = 0
}

files := make([]*snips.File, 0)
rows, err := s.QueryContext(ctx, query, page)
if err != nil {
return files, err
}

for rows.Next() {
file := &snips.File{}
if err := rows.Scan(
&file.ID,
&file.CreatedAt,
&file.UpdatedAt,
&file.Size,
&file.Private,
&file.Type,
&file.UserID,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}

return nil, err
}

files = append(files, file)
}

return files, nil
}

func (s *Sqlite) FindFilesByUser(ctx context.Context, userID string) ([]*snips.File, error) {
// note that content is _not_ included
const query = `
Expand Down
56 changes: 56 additions & 0 deletions internal/http/api/v1/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package api_v1

import (
"encoding/json"
"net/http"
"strconv"

"github.com/go-chi/chi/v5"
"github.com/robherley/snips.sh/internal/config"
"github.com/robherley/snips.sh/internal/db"
"github.com/robherley/snips.sh/internal/logger"
)

// GetSnips
// Retrieve a list of snips on the server, paginated, in date created DESC by
// default.
// Ideal utilisation, when trying to view all snips, is to iterate through pages
// until you reach a payload size of 0.
func GetSnips(database db.DB) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
log := logger.From(r.Context())

page := 0
if r.URL.Query().Has("page") {
strPage := r.URL.Query().Get("page")
p, err := strconv.Atoi(strPage)
if err == nil {
page = p
}
}

files, err := database.FindFiles(r.Context(), page)
if err != nil {
log.Error().Err(err).Msg("unable to render template")
http.Error(w, "internal error", http.StatusInternalServerError)
return
}

filesMarshalled, err := json.Marshal(files)
if err != nil {
log.Error().Err(err).Msg("unable to render template")
http.Error(w, "internal error", http.StatusInternalServerError)
return
}

w.Write(filesMarshalled)
}
}

func ApiHandler(cfg *config.Config, database db.DB) *chi.Mux {
apiRouter := chi.NewMux()

apiRouter.Get("/snips", GetSnips(database))

return apiRouter
}
1 change: 1 addition & 0 deletions internal/http/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (

jsFiles = []string{
"snips.js",
"list.js",
}
)

Expand Down
32 changes: 32 additions & 0 deletions internal/http/handlers.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package http

import (
"bytes"
"encoding/json"
"html/template"
"io"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -49,6 +51,36 @@ func MetaHandler(cfg *config.Config) http.HandlerFunc {
}
}

func ListHandler(config *config.Config, database db.DB, assets Assets) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var err error

log := logger.From(r.Context())

buff := bytes.NewBuffer([]byte{})
assets.Template().ExecuteTemplate(buff, "list.go.html", nil)
html, _ := io.ReadAll(buff)

vars := map[string]interface{}{
"FileID": "Latest snips",
"FileSize": nil,
"CreatedAt": nil,
"UpdatedAt": nil,
"FileType": nil,
"RawHREF": "",
"HTML": template.HTML(string(html)),
"Private": false,
}

err = assets.Template().ExecuteTemplate(w, "file.go.html", vars)
if err != nil {
log.Error().Err(err).Msg("unable to render template")
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
}
}

func DocHandler(assets Assets) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log := logger.From(r.Context())
Expand Down
17 changes: 16 additions & 1 deletion internal/http/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/robherley/snips.sh/internal/config"
"github.com/robherley/snips.sh/internal/db"
api_v1 "github.com/robherley/snips.sh/internal/http/api/v1"
"github.com/rs/zerolog/log"
)

type Service struct {
Expand All @@ -22,14 +24,27 @@ func New(cfg *config.Config, database db.DB, assets Assets) (*Service, error) {
router.Use(WithMetrics)
router.Use(WithRecover)

router.Get("/", DocHandler(assets))
if cfg.ListIndex && cfg.EnableApi {
router.Get("/", ListHandler(cfg, database, assets))
} else {
if cfg.ListIndex && !cfg.EnableApi {
log.Warn().Bool("ListIndex", cfg.ListIndex).Bool("EnableApi", cfg.EnableApi).Msg("cannot list index without enabling api")
}

router.Get("/", DocHandler(assets))
}

router.Get("/docs/{name}", DocHandler(assets))
router.Get("/health", HealthHandler)
router.Get("/f/{fileID}", FileHandler(cfg, database, assets))
router.Get("/assets/index.js", assets.ServeJS)
router.Get("/assets/index.css", assets.ServeCSS)
router.Get("/meta.json", MetaHandler(cfg))

if cfg.EnableApi {
router.Mount("/api/v1", api_v1.ApiHandler(cfg, database))
}

if cfg.Debug {
router.Mount("/_debug", middleware.Profiler())
}
Expand Down
47 changes: 47 additions & 0 deletions internal/ssh/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ type UploadFlags struct {
Private bool
Extension string
TTL time.Duration
Name string
}

func (uf *UploadFlags) Parse(out io.Writer, args []string) error {
uf.FlagSet = flag.NewFlagSet("", flag.ContinueOnError)
uf.SetOutput(out)

uf.StringVar(&uf.Name, "name", "", "define custom name, if unavailable then a unique hash will be appended")
uf.BoolVar(&uf.Private, "private", false, "only accessible via creator or signed urls (optional)")
uf.StringVar(&uf.Extension, "ext", "", "set the file extension (optional)")
addDurationFlag(uf.FlagSet, &uf.TTL, "ttl", 0, "lifetime of the signed url (optional)")
Expand All @@ -41,6 +43,26 @@ func (uf *UploadFlags) Parse(out io.Writer, args []string) error {
}

uf.Extension = strings.TrimPrefix(strings.ToLower(uf.Extension), ".")
uf.Name = strings.Trim(uf.Name, " ")

return nil
}

type RenameFlags struct {
*flag.FlagSet

Name string
}

func (sf *RenameFlags) Parse(out io.Writer, args []string) error {
sf.FlagSet = flag.NewFlagSet("", flag.ContinueOnError)
sf.SetOutput(out)

addStringFlag(sf.FlagSet, &sf.Name, "name", "", "define custom name, if unavailable then a unique hash will be appended")

if err := sf.FlagSet.Parse(args); err != nil {
return err
}

return nil
}
Expand Down Expand Up @@ -111,3 +133,28 @@ func (d *durationFlagValue) Get() any {
func (d *durationFlagValue) String() string {
return (*time.Duration)(d).String()
}

// stringFlagValue is a wrapper around string that implements the flag.Value interface using a custom parser.
type stringFlagValue string

// addStringFlag adds a flag for a string to the given flag.FlagSet.
func addStringFlag(fs *flag.FlagSet, flagValue *string, name string, value string, usage string) {
*flagValue = value
fs.Var((*stringFlagValue)(flagValue), name, usage)
}

// Set implements the flag.Value interface.
func (sf *stringFlagValue) Set(s string) error {
*sf = stringFlagValue(s)
return nil
}

// Get implements the flag.Getter interface.
func (sf *stringFlagValue) Get() any {
return string(*sf)
}

// String implements the flag.Value interface.
func (sf *stringFlagValue) String() string {
return string(*sf)
}
Loading

0 comments on commit b02203b

Please sign in to comment.