this repo has no description smallweb.run
smallweb
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

remove cron, and use file for sessions

pomdtr 52033610 f0649898

+525 -1399
-3
.vscode/settings.json
··· 1 1 { 2 2 "deno.enable": true, 3 - "deno.disablePaths": [ 4 - "term" 5 - ], 6 3 "search.exclude": { 7 4 "**/dist": true, 8 5 },
+4 -6
Dockerfile
··· 13 13 && chmod +x /usr/local/bin/smallweb 14 14 15 15 # Set environment variables 16 - ENV SMALLWEB_DATA_DIR=/var/lib/smallweb \ 17 - SMALLWEB_DIR=/smallweb \ 18 - SMALLWEB_HOST=0.0.0.0 \ 19 - SMALLWEB_PORT=7777 16 + ENV SMALLWEB_DIR=/smallweb \ 17 + SMALLWEB_ADDR=0.0.0.0:7777 20 18 21 19 # Create necessary directories and set permissions 22 - RUN mkdir -p "$SMALLWEB_DATA_DIR" "$SMALLWEB_DIR" \ 20 + RUN mkdir -p "$SMALLWEB_DIR" \ 23 21 && chown -R smallweb:smallweb "$SMALLWEB_DATA_DIR" "$SMALLWEB_DIR" 24 22 25 23 # Switch to non-root user 26 24 USER smallweb 27 25 28 26 # Declare volumes 29 - VOLUME ["$SMALLWEB_DATA_DIR", "$SMALLWEB_DIR"] 27 + VOLUME ["$SMALLWEB_DIR"] 30 28 31 29 # Expose port 32 30 EXPOSE 7777
+14 -73
api/api.go
··· 15 15 16 16 "github.com/adrg/xdg" 17 17 "github.com/getkin/kin-openapi/openapi3" 18 - "github.com/knadh/koanf/v2" 19 18 "github.com/pomdtr/smallweb/app" 20 19 "github.com/pomdtr/smallweb/utils" 21 20 "golang.org/x/net/webdav" ··· 34 33 var swagger embed.FS 35 34 36 35 type Server struct { 37 - k *koanf.Koanf 36 + domain string 37 + dir string 38 38 httpWriter *utils.MultiWriter 39 - cronWriter *utils.MultiWriter 40 39 consoleWriter *utils.MultiWriter 41 40 } 42 41 43 - func NewHandler(k *koanf.Koanf, httpWriter *utils.MultiWriter, cronWriter *utils.MultiWriter, consoleWriter *utils.MultiWriter) http.Handler { 44 - server := &Server{k: k, httpWriter: httpWriter, cronWriter: cronWriter, consoleWriter: consoleWriter} 42 + func NewHandler(dir string, domain string, httpWriter *utils.MultiWriter, consoleWriter *utils.MultiWriter) http.Handler { 43 + server := &Server{domain: domain, dir: dir, httpWriter: httpWriter, consoleWriter: consoleWriter} 45 44 handler := Handler(server) 46 45 webdavHandler := webdav.Handler{ 47 - FileSystem: webdav.Dir(k.String("dir")), 46 + FileSystem: webdav.Dir(dir), 48 47 LockSystem: webdav.NewMemLS(), 49 48 Prefix: "/webdav", 50 49 } 51 50 52 51 caddyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 - domain := r.URL.Query().Get("domain") 54 - if domain == k.String("domain") { 52 + d := r.URL.Query().Get("domain") 53 + if d == domain { 55 54 w.WriteHeader(http.StatusOK) 56 55 w.Write([]byte("OK")) 57 56 return 58 57 } 59 58 60 - if !strings.HasSuffix(domain, "."+k.String("domain")) { 59 + if !strings.HasSuffix(d, "."+domain) { 61 60 http.Error(w, "invalid domain", http.StatusBadRequest) 62 61 return 63 62 } 64 63 65 - appname := strings.TrimSuffix(domain, "."+k.String("domain")) 66 - appDir := filepath.Join(k.String("dir"), appname) 67 - if _, err := app.LoadApp(appDir, k.String("domain")); err != nil { 64 + appname := strings.TrimSuffix(d, "."+domain) 65 + appDir := filepath.Join(dir, appname) 66 + if _, err := app.LoadApp(appDir, domain); err != nil { 68 67 http.Error(w, err.Error(), http.StatusInternalServerError) 69 68 return 70 69 } ··· 129 128 130 129 // GetV0AppsAppEnv implements ServerInterface. 131 130 func (me *Server) GetApp(w http.ResponseWriter, r *http.Request, appname string) { 132 - a, err := app.LoadApp(filepath.Join(me.k.String("dir"), appname), me.k.String("domain")) 131 + a, err := app.LoadApp(filepath.Join(me.dir, appname), me.domain) 133 132 if err != nil { 134 133 http.Error(w, err.Error(), http.StatusInternalServerError) 135 134 return ··· 176 175 } 177 176 178 177 func (me *Server) GetApps(w http.ResponseWriter, r *http.Request) { 179 - rootDir := me.k.String("dir") 180 - names, err := app.ListApps(me.k.String("dir")) 178 + names, err := app.ListApps(me.dir) 181 179 if err != nil { 182 180 http.Error(w, err.Error(), http.StatusInternalServerError) 183 181 return ··· 185 183 186 184 var apps []App 187 185 for _, name := range names { 188 - a, err := app.LoadApp(filepath.Join(rootDir, name), me.k.String("domain")) 186 + a, err := app.LoadApp(filepath.Join(me.dir, name), me.domain) 189 187 if err != nil { 190 188 http.Error(w, err.Error(), http.StatusInternalServerError) 191 189 return ··· 325 323 } 326 324 327 325 if host != *params.Host { 328 - continue 329 - } 330 - 331 - w.Write(logMsg) 332 - flusher.Flush() // Push data to the client 333 - case <-r.Context().Done(): 334 - // If the client disconnects, stop the loop 335 - return 336 - } 337 - } 338 - } 339 - 340 - func (me *Server) GetCronLogs(w http.ResponseWriter, r *http.Request, params GetCronLogsParams) { 341 - if me.cronWriter == nil { 342 - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) 343 - return 344 - } 345 - 346 - flusher, ok := w.(http.Flusher) 347 - if !ok { 348 - http.Error(w, "Streaming unsupported", http.StatusInternalServerError) 349 - return 350 - } 351 - 352 - // Set the necessary headers for SSE 353 - w.Header().Set("Content-Type", "application/octet-stream") 354 - w.Header().Set("Cache-Control", "no-cache") 355 - w.Header().Set("Connection", "keep-alive") 356 - 357 - // Create a new channel for this client to receive logs 358 - clientChan := make(chan []byte) 359 - me.httpWriter.AddClient(clientChan) 360 - defer me.httpWriter.RemoveClient(clientChan) 361 - 362 - // Listen to the client channel and send logs to the client 363 - for { 364 - select { 365 - case logMsg := <-clientChan: 366 - // Send the log message as SSE event 367 - if params.App == nil { 368 - w.Write(logMsg) 369 - flusher.Flush() // Push data to the client 370 - continue 371 - } 372 - 373 - var log map[string]any 374 - if err := json.Unmarshal(logMsg, &log); err != nil { 375 - fmt.Fprintln(os.Stderr, "failed to parse log:", err) 376 - continue 377 - } 378 - 379 - app, ok := log["app"].(string) 380 - if !ok { 381 - continue 382 - } 383 - 384 - if app != *params.App { 385 326 continue 386 327 } 387 328
+10 -160
api/gen.go
··· 43 43 App *string `form:"app,omitempty" json:"app,omitempty"` 44 44 } 45 45 46 - // GetCronLogsParams defines parameters for GetCronLogs. 47 - type GetCronLogsParams struct { 48 - // App Filter logs by app 49 - App *string `form:"app,omitempty" json:"app,omitempty"` 50 - } 51 - 52 46 // GetHttpLogsParams defines parameters for GetHttpLogs. 53 47 type GetHttpLogsParams struct { 54 48 // Host Filter logs by host ··· 145 139 // GetConsoleLogs request 146 140 GetConsoleLogs(ctx context.Context, params *GetConsoleLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) 147 141 148 - // GetCronLogs request 149 - GetCronLogs(ctx context.Context, params *GetCronLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) 150 - 151 142 // GetHttpLogs request 152 143 GetHttpLogs(ctx context.Context, params *GetHttpLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) 153 144 ··· 183 174 184 175 func (c *Client) GetConsoleLogs(ctx context.Context, params *GetConsoleLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { 185 176 req, err := NewGetConsoleLogsRequest(c.Server, params) 186 - if err != nil { 187 - return nil, err 188 - } 189 - req = req.WithContext(ctx) 190 - if err := c.applyEditors(ctx, req, reqEditors); err != nil { 191 - return nil, err 192 - } 193 - return c.Client.Do(req) 194 - } 195 - 196 - func (c *Client) GetCronLogs(ctx context.Context, params *GetCronLogsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { 197 - req, err := NewGetCronLogsRequest(c.Server, params) 198 177 if err != nil { 199 178 return nil, err 200 179 } ··· 351 330 return req, nil 352 331 } 353 332 354 - // NewGetCronLogsRequest generates requests for GetCronLogs 355 - func NewGetCronLogsRequest(server string, params *GetCronLogsParams) (*http.Request, error) { 356 - var err error 357 - 358 - serverURL, err := url.Parse(server) 359 - if err != nil { 360 - return nil, err 361 - } 362 - 363 - operationPath := fmt.Sprintf("/v0/logs/cron") 364 - if operationPath[0] == '/' { 365 - operationPath = "." + operationPath 366 - } 367 - 368 - queryURL, err := serverURL.Parse(operationPath) 369 - if err != nil { 370 - return nil, err 371 - } 372 - 373 - if params != nil { 374 - queryValues := queryURL.Query() 375 - 376 - if params.App != nil { 377 - 378 - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "app", runtime.ParamLocationQuery, *params.App); err != nil { 379 - return nil, err 380 - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { 381 - return nil, err 382 - } else { 383 - for k, v := range parsed { 384 - for _, v2 := range v { 385 - queryValues.Add(k, v2) 386 - } 387 - } 388 - } 389 - 390 - } 391 - 392 - queryURL.RawQuery = queryValues.Encode() 393 - } 394 - 395 - req, err := http.NewRequest("GET", queryURL.String(), nil) 396 - if err != nil { 397 - return nil, err 398 - } 399 - 400 - return req, nil 401 - } 402 - 403 333 // NewGetHttpLogsRequest generates requests for GetHttpLogs 404 334 func NewGetHttpLogsRequest(server string, params *GetHttpLogsParams) (*http.Request, error) { 405 335 var err error ··· 548 478 // GetConsoleLogsWithResponse request 549 479 GetConsoleLogsWithResponse(ctx context.Context, params *GetConsoleLogsParams, reqEditors ...RequestEditorFn) (*GetConsoleLogsResponse, error) 550 480 551 - // GetCronLogsWithResponse request 552 - GetCronLogsWithResponse(ctx context.Context, params *GetCronLogsParams, reqEditors ...RequestEditorFn) (*GetCronLogsResponse, error) 553 - 554 481 // GetHttpLogsWithResponse request 555 482 GetHttpLogsWithResponse(ctx context.Context, params *GetHttpLogsParams, reqEditors ...RequestEditorFn) (*GetHttpLogsResponse, error) 556 483 ··· 625 552 return 0 626 553 } 627 554 628 - type GetCronLogsResponse struct { 629 - Body []byte 630 - HTTPResponse *http.Response 631 - } 632 - 633 - // Status returns HTTPResponse.Status 634 - func (r GetCronLogsResponse) Status() string { 635 - if r.HTTPResponse != nil { 636 - return r.HTTPResponse.Status 637 - } 638 - return http.StatusText(0) 639 - } 640 - 641 - // StatusCode returns HTTPResponse.StatusCode 642 - func (r GetCronLogsResponse) StatusCode() int { 643 - if r.HTTPResponse != nil { 644 - return r.HTTPResponse.StatusCode 645 - } 646 - return 0 647 - } 648 - 649 555 type GetHttpLogsResponse struct { 650 556 Body []byte 651 557 HTTPResponse *http.Response ··· 714 620 return nil, err 715 621 } 716 622 return ParseGetConsoleLogsResponse(rsp) 717 - } 718 - 719 - // GetCronLogsWithResponse request returning *GetCronLogsResponse 720 - func (c *ClientWithResponses) GetCronLogsWithResponse(ctx context.Context, params *GetCronLogsParams, reqEditors ...RequestEditorFn) (*GetCronLogsResponse, error) { 721 - rsp, err := c.GetCronLogs(ctx, params, reqEditors...) 722 - if err != nil { 723 - return nil, err 724 - } 725 - return ParseGetCronLogsResponse(rsp) 726 623 } 727 624 728 625 // GetHttpLogsWithResponse request returning *GetHttpLogsResponse ··· 819 716 return response, nil 820 717 } 821 718 822 - // ParseGetCronLogsResponse parses an HTTP response from a GetCronLogsWithResponse call 823 - func ParseGetCronLogsResponse(rsp *http.Response) (*GetCronLogsResponse, error) { 824 - bodyBytes, err := io.ReadAll(rsp.Body) 825 - defer func() { _ = rsp.Body.Close() }() 826 - if err != nil { 827 - return nil, err 828 - } 829 - 830 - response := &GetCronLogsResponse{ 831 - Body: bodyBytes, 832 - HTTPResponse: rsp, 833 - } 834 - 835 - return response, nil 836 - } 837 - 838 719 // ParseGetHttpLogsResponse parses an HTTP response from a GetHttpLogsWithResponse call 839 720 func ParseGetHttpLogsResponse(rsp *http.Response) (*GetHttpLogsResponse, error) { 840 721 bodyBytes, err := io.ReadAll(rsp.Body) ··· 891 772 892 773 // (GET /v0/logs/console) 893 774 GetConsoleLogs(w http.ResponseWriter, r *http.Request, params GetConsoleLogsParams) 894 - 895 - // (GET /v0/logs/cron) 896 - GetCronLogs(w http.ResponseWriter, r *http.Request, params GetCronLogsParams) 897 775 898 776 // (GET /v0/logs/http) 899 777 GetHttpLogs(w http.ResponseWriter, r *http.Request, params GetHttpLogsParams) ··· 977 855 handler.ServeHTTP(w, r) 978 856 } 979 857 980 - // GetCronLogs operation middleware 981 - func (siw *ServerInterfaceWrapper) GetCronLogs(w http.ResponseWriter, r *http.Request) { 982 - 983 - var err error 984 - 985 - // Parameter object where we will unmarshal all parameters from the context 986 - var params GetCronLogsParams 987 - 988 - // ------------- Optional query parameter "app" ------------- 989 - 990 - err = runtime.BindQueryParameter("form", true, false, "app", r.URL.Query(), &params.App) 991 - if err != nil { 992 - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "app", Err: err}) 993 - return 994 - } 995 - 996 - handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 997 - siw.Handler.GetCronLogs(w, r, params) 998 - })) 999 - 1000 - for _, middleware := range siw.HandlerMiddlewares { 1001 - handler = middleware(handler) 1002 - } 1003 - 1004 - handler.ServeHTTP(w, r) 1005 - } 1006 - 1007 858 // GetHttpLogs operation middleware 1008 859 func (siw *ServerInterfaceWrapper) GetHttpLogs(w http.ResponseWriter, r *http.Request) { 1009 860 ··· 1179 1030 m.HandleFunc("GET "+options.BaseURL+"/v0/apps", wrapper.GetApps) 1180 1031 m.HandleFunc("GET "+options.BaseURL+"/v0/apps/{app}", wrapper.GetApp) 1181 1032 m.HandleFunc("GET "+options.BaseURL+"/v0/logs/console", wrapper.GetConsoleLogs) 1182 - m.HandleFunc("GET "+options.BaseURL+"/v0/logs/cron", wrapper.GetCronLogs) 1183 1033 m.HandleFunc("GET "+options.BaseURL+"/v0/logs/http", wrapper.GetHttpLogs) 1184 1034 m.HandleFunc("POST "+options.BaseURL+"/v0/run/{app}", wrapper.RunApp) 1185 1035 ··· 1189 1039 // Base64 encoded, gzipped, json marshaled Swagger object 1190 1040 var swaggerSpec = []string{ 1191 1041 1192 - "H4sIAAAAAAAC/+SWzW7bMAyAX8XgdvTqYL351hVYV6DAhvY49KDYjKPCFlWK6hYEfveBctPGsdvur4dh", 1193 - "pwik+PeJJrOFijpPDp0EKLcQqjV2Jh1PvNcfz+SRxWISdsbZFQbRs2w8Qgm0vMFKoM/BmQ73FEHYukYV", 1194 - "kdsZeZ8D4220jDWUXwfr4e51PnV+Sl1nXP05io8yTayiej+2dYINshoGqZF5Nq8gNUWZV8WqwhD2dEui", 1195 - "Fo2b5L27mQ85PHh9iDytRl1Yt6Lk3UqruqvOtO03XGYnX84hhzvkYMlBCQtNhzw64y2UcHy0ODqGHLyR", 1196 - "dUqvuFsUxvt0bjBVo2CMWHLnNZRwhnKiek07eHJhIPZ+sRjAOUGXzIz3ra2SYXETNPiuIfRkBbtk+JZx", 1197 - "BSW8KR5bp7jvm0Kbpn+o1zCbzVBujaFi62Wo6cIGyWiVpbxVLaYJCjMJrlWyK6vYGu/7F4pLPNh0KMjq", 1198 - "ZwtWwygj2PWl+ob9pxOOmO8VeNie138I7EVOUy5nKMrkGSQtNaGoyAVq8Tkop8OVC2rCFM446EfbCnKm", 1199 - "nrPlJhswJX63EXlzCPBvAaNKUN4FYTTdGNyKuDOin5x1JiVwGGkC7iq5STWM4CXBATweHupJckzuP8NG", 1200 - "q0yp/BS+tYh/Dt8nEf8b+NYU5Al+96p/uO84uscx5inMkLuM7lXn2G3EIB+o3vzSCBvvWMNNGG2CydY8", 1201 - "mPvjRZnM55Zh/4qDdvy3IT0RfpfCt8YeeHnxrS+j0688q1o7O6D7/kcAAAD//yG73PJOCQAA", 1042 + "H4sIAAAAAAAC/9SVTW/bPAyA/4rB9z16dbDefOsKrCtQYEN7HHpQbMZRYYuqSHULAv/3gXLTxrGXYB89", 1043 + "7GSBFL8ekfQWKuo8OXTCUG6BqzV2Jh0vvNePD+QxiMUk7IyzK2TRs2w8Qgm0fMBKoM/BmQ73FCzBukYV", 1044 + "MbQz8j6HgI/RBqyh/DpYD3fv86nzS+o64+rPUXyUaWIV1fuxrRNsMKghS40hzObFUlOUeVWsKmTe0y2J", 1045 + "WjRukvfuZj7k8OL1JfK0GnVh3YqSdyut6u4607bfcJldfLmGHJ4wsCUHJSw0HfLojLdQwvnZ4uwccvBG", 1046 + "1im94mlRGO/TucFUjYIxYsld11DCFcqF6jVt9uR4IPZ+sRjAOUGXzIz3ra2SYfHAGnzXEHqygl0y/D/g", 1047 + "Ckr4r3htneK5bwptmv6lXhOC2Qzl1shVsF6Gmm4sS0arLOWtajENK8wkuFfJrqxia7zvTxSXeATToWBQ", 1048 + "P1uwGkYZwa4v1TfsP52EiPlegYftef+HwE5ymnK5QlEmR5C01HBRkWNq8RiUy+HKDTU8hTMO+tG2giFT", 1049 + "z9lykw2YEr/HiGFzCPBvAaNKUN6xBDTdGNyKQmdER846kxI4jDQBd5fcpBpG8JJgDG8t4o+R+yTifwPb", 1050 + "mlh+wu1Z9Q+DC9G9zqEnniF3G92bDuJjRJYPVG9+aQbHPwkTGh6tssnaP1hc402fzOe2ef+Gm2L830tP", 1051 + "hN+l8K2xB15OvvVtdDrdWdXa2Q3T9z8CAAD//4ItGTEPCAAA", 1202 1052 } 1203 1053 1204 1054 // GetSwagger returns the content of the embedded swagger specification file
-31
api/openapi.json
··· 113 113 } 114 114 } 115 115 }, 116 - "/v0/logs/cron": { 117 - "get": { 118 - "tags": [ 119 - "logs" 120 - ], 121 - "operationId": "getCronLogs", 122 - "parameters": [ 123 - { 124 - "name": "app", 125 - "in": "query", 126 - "schema": { 127 - "type": "string" 128 - }, 129 - "description": "Filter logs by app" 130 - } 131 - ], 132 - "responses": { 133 - "200": { 134 - "description": "Stream of cron logs", 135 - "content": { 136 - "application/octet-stream": { 137 - "schema": { 138 - "type": "string", 139 - "format": "binary" 140 - } 141 - } 142 - } 143 - } 144 - } 145 - } 146 - }, 147 116 "/v0/logs/http": { 148 117 "get": { 149 118 "operationId": "getHttpLogs",
-33
api/schemas/manifest.schema.json
··· 22 22 "items": { 23 23 "type": "string" 24 24 } 25 - }, 26 - "crons": { 27 - "type": "array", 28 - "description": "Set of cron jobs", 29 - "items": { 30 - "type": "object", 31 - "required": [ 32 - "name", 33 - "schedule", 34 - "args" 35 - ], 36 - "properties": { 37 - "name": { 38 - "type": "string", 39 - "description": "Name of the cron job" 40 - }, 41 - "description": { 42 - "type": "string", 43 - "description": "Description of the cron job" 44 - }, 45 - "schedule": { 46 - "type": "string", 47 - "description": "Cron schedule" 48 - }, 49 - "args": { 50 - "type": "array", 51 - "description": "Arguments to pass to the script", 52 - "items": { 53 - "type": "string" 54 - } 55 - } 56 - } 57 - } 58 25 } 59 26 } 60 27 }
+5 -14
app/app.go
··· 12 12 "github.com/tailscale/hujson" 13 13 ) 14 14 15 - type CronJob struct { 16 - Name string `json:"name"` 17 - Description string `json:"description"` 18 - Schedule string `json:"schedule"` 19 - Args []string `json:"args"` 20 - } 21 - 22 15 type AppConfig struct { 23 - Entrypoint string `json:"entrypoint,omitempty"` 24 - Root string `json:"root,omitempty"` 25 - Private bool `json:"private,omitempty"` 26 - PublicRoutes []string `json:"publicRoutes,omitempty"` 27 - PrivateRoutes []string `json:"privateRoutes,omitempty"` 28 - Crons []CronJob `json:"crons,omitempty"` 16 + Entrypoint string `json:"entrypoint,omitempty"` 17 + Root string `json:"root,omitempty"` 18 + Private bool `json:"private,omitempty"` 19 + PublicRoutes []string `json:"publicRoutes,omitempty"` 20 + PrivateRoutes []string `json:"privateRoutes,omitempty"` 29 21 } 30 22 31 23 type App struct { ··· 79 71 Url: fmt.Sprintf("https://%s.%s/", name, domain), 80 72 Env: make(map[string]string), 81 73 Config: AppConfig{ 82 - Crons: make([]CronJob, 0), 83 74 PublicRoutes: make([]string, 0), 84 75 PrivateRoutes: make([]string, 0), 85 76 },
+8 -93
auth/auth.go
··· 1 1 package auth 2 2 3 3 import ( 4 - "database/sql" 5 4 "encoding/json" 6 5 "fmt" 7 6 "log" 8 7 "net/http" 9 8 "net/url" 10 - "slices" 11 9 "strings" 12 10 "time" 13 11 14 - gonanoid "github.com/matoous/go-nanoid/v2" 15 12 "github.com/mssola/user_agent" 16 - "github.com/pomdtr/smallweb/database" 17 13 "github.com/pomdtr/smallweb/utils" 18 - "golang.org/x/crypto/bcrypt" 19 14 "golang.org/x/oauth2" 20 15 ) 21 16 22 - func CreateSession(db *sql.DB, email string, domain string) (string, error) { 23 - sessionID, err := gonanoid.New() 24 - if err != nil { 25 - return "", fmt.Errorf("failed to generate session ID: %w", err) 26 - } 27 - 28 - session := database.Session{ 29 - ID: sessionID, 30 - Email: email, 31 - Domain: domain, 32 - CreatedAt: time.Now(), 33 - ExpiresAt: time.Now().Add(14 * 24 * time.Hour), 34 - } 35 - 36 - if err := database.InsertSession(db, &session); err != nil { 37 - return "", fmt.Errorf("failed to insert session: %w", err) 38 - } 39 - 40 - return sessionID, nil 41 - } 42 - 43 - func DeleteSession(db *sql.DB, sessionID string) error { 44 - if err := database.DeleteSession(db, sessionID); err != nil { 45 - return fmt.Errorf("failed to delete session: %w", err) 46 - } 47 - 48 - return nil 49 - } 50 - 51 - func GetSession(db *sql.DB, sessionID string, domain string) (database.Session, error) { 52 - session, err := database.GetSession(db, sessionID) 53 - if err != nil { 54 - return database.Session{}, fmt.Errorf("failed to get session: %w", err) 55 - } 56 - 57 - if session.Domain != domain { 58 - return database.Session{}, fmt.Errorf("session not found") 59 - } 60 - 61 - return *session, nil 62 - } 63 - 64 - func ExtendSession(db *sql.DB, sessionID string, expiresAt time.Time) error { 65 - session, err := database.GetSession(db, sessionID) 66 - if err != nil { 67 - return fmt.Errorf("failed to get session: %w", err) 68 - } 69 - 70 - session.ExpiresAt = expiresAt 71 - if err := database.UpdateSession(db, session); err != nil { 72 - return fmt.Errorf("failed to update session: %w", err) 73 - } 74 - 75 - return nil 76 - } 77 - 78 - func VerifyToken(db *sql.DB, token string, appname string) error { 79 - public, secret, err := database.ParseToken(token) 80 - if err != nil { 81 - return err 82 - } 83 - 84 - t, err := database.GetToken(db, public) 85 - if err != nil { 86 - return err 87 - } 88 - 89 - if !t.Admin { 90 - if !slices.Contains(t.Apps, appname) { 91 - return fmt.Errorf("app not authorized") 92 - } 93 - } 94 - 95 - if err := bcrypt.CompareHashAndPassword([]byte(t.Hash), []byte(secret)); err != nil { 96 - return err 97 - } 98 - 99 - return nil 100 - } 101 - 102 17 func isBrowser(ua *user_agent.UserAgent) bool { 103 18 if ua.Bot() { 104 19 return false ··· 125 40 return false 126 41 } 127 42 128 - func Middleware(db *sql.DB, provider string, email string, appname string) func(http.Handler) http.Handler { 43 + func Middleware(provider string, email string, appname string) func(http.Handler) http.Handler { 129 44 return func(next http.Handler) http.Handler { 130 45 sessionCookieName := "smallweb-session" 131 46 oauthCookieName := "smallweb-oauth-store" ··· 137 52 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 138 53 username, _, ok := r.BasicAuth() 139 54 if ok { 140 - if err := VerifyToken(db, username, appname); err != nil { 55 + if err := VerifyToken(username, appname); err != nil { 141 56 w.Header().Add("WWW-Authenticate", `Basic realm="smallweb"`) 142 57 // here we return unauthorized instead of forbidden to trigger the basic auth prompt 143 58 http.Error(w, "Unauthorized", http.StatusUnauthorized) ··· 151 66 authorization := r.Header.Get("Authorization") 152 67 if strings.HasPrefix(authorization, "Bearer ") { 153 68 token := strings.TrimPrefix(authorization, "Bearer ") 154 - if err := VerifyToken(db, token, appname); err != nil { 69 + if err := VerifyToken(token, appname); err != nil { 155 70 w.Header().Add("WWW-Authenticate", `Basic realm="smallweb"`) 156 71 http.Error(w, "Forbidden", http.StatusForbidden) 157 72 return ··· 297 212 return 298 213 } 299 214 300 - sessionID, err := CreateSession(db, userinfo.Email, r.Host) 215 + sessionID, err := CreateSession(userinfo.Email, r.Host) 301 216 if err != nil { 302 217 log.Printf("failed to create session: %v", err) 303 218 http.Error(w, "Unauthorized", http.StatusUnauthorized) ··· 337 252 return 338 253 } 339 254 340 - if err := DeleteSession(db, cookie.Value); err != nil { 255 + if err := DeleteSession(cookie.Value); err != nil { 341 256 log.Printf("failed to delete session: %v", err) 342 257 http.Error(w, "Unauthorized", http.StatusUnauthorized) 343 258 return ··· 367 282 return 368 283 } 369 284 370 - session, err := GetSession(db, cookie.Value, r.Host) 285 + session, err := GetSession(cookie.Value, r.Host) 371 286 if err != nil { 372 287 http.SetCookie(w, &http.Cookie{ 373 288 Name: sessionCookieName, ··· 382 297 } 383 298 384 299 if time.Now().After(session.ExpiresAt) { 385 - if err := DeleteSession(db, cookie.Value); err != nil { 300 + if err := DeleteSession(cookie.Value); err != nil { 386 301 http.Error(w, "Internal Server Error", http.StatusInternalServerError) 387 302 return 388 303 } ··· 407 322 408 323 // if session is near expiration, extend it 409 324 if time.Now().Add(7 * 24 * time.Hour).After(session.ExpiresAt) { 410 - if err := ExtendSession(db, cookie.Value, time.Now().Add(14*24*time.Hour)); err != nil { 325 + if err := ExtendSession(cookie.Value, time.Now().Add(14*24*time.Hour)); err != nil { 411 326 http.Error(w, "Internal Server Error", http.StatusInternalServerError) 412 327 return 413 328 }
+154
auth/session.go
··· 1 + package auth 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + "time" 9 + 10 + gonanoid "github.com/matoous/go-nanoid/v2" 11 + "github.com/pomdtr/smallweb/utils" 12 + ) 13 + 14 + type Session struct { 15 + ID string `json:"id"` 16 + Email string `json:"email"` 17 + Domain string `json:"domain"` 18 + CreatedAt time.Time `json:"createdAt"` 19 + ExpiresAt time.Time `json:"expiresAt"` 20 + } 21 + 22 + func sessionDir() string { 23 + return filepath.Join(utils.DataDir(), "sessions") 24 + } 25 + 26 + func sessionPath(sessionID string) string { 27 + return filepath.Join(sessionDir(), fmt.Sprintf("%s.json", sessionID)) 28 + } 29 + 30 + func CreateSession(email string, domain string) (string, error) { 31 + sessionID, err := gonanoid.New() 32 + if err != nil { 33 + return "", fmt.Errorf("failed to generate session ID: %w", err) 34 + } 35 + 36 + session := Session{ 37 + ID: sessionID, 38 + Email: email, 39 + Domain: domain, 40 + CreatedAt: time.Now(), 41 + ExpiresAt: time.Now().Add(14 * 24 * time.Hour), 42 + } 43 + 44 + sessionPath := sessionPath(sessionID) 45 + sessionBytes, err := json.Marshal(session) 46 + if err != nil { 47 + return "", fmt.Errorf("failed to marshal session: %w", err) 48 + } 49 + 50 + if err := os.WriteFile(sessionPath, sessionBytes, 0600); err != nil { 51 + return "", fmt.Errorf("failed to write session: %w", err) 52 + } 53 + 54 + if err := DeleteExpiredSessions(); err != nil { 55 + return "", fmt.Errorf("failed to delete expired sessions: %w", err) 56 + } 57 + 58 + return sessionID, nil 59 + } 60 + 61 + func SaveSession(session Session) error { 62 + sessionPath := sessionPath(session.ID) 63 + sessionBytes, err := json.Marshal(session) 64 + if err != nil { 65 + return fmt.Errorf("failed to marshal session: %w", err) 66 + } 67 + 68 + if err := os.WriteFile(sessionPath, sessionBytes, 0600); err != nil { 69 + return fmt.Errorf("failed to write session: %w", err) 70 + } 71 + 72 + return nil 73 + } 74 + 75 + func DeleteSession(sessionID string) error { 76 + sessionPath := sessionPath(sessionID) 77 + if err := os.Remove(sessionPath); err != nil { 78 + return fmt.Errorf("failed to remove session: %w", err) 79 + } 80 + 81 + return nil 82 + } 83 + 84 + func DeleteExpiredSessions() error { 85 + entries, err := os.ReadDir(sessionDir()) 86 + if err != nil { 87 + return fmt.Errorf("failed to read sessions: %w", err) 88 + } 89 + 90 + for _, entry := range entries { 91 + sessionPath := filepath.Join(sessionDir(), entry.Name()) 92 + sessionBytes, err := os.ReadFile(sessionPath) 93 + if err != nil { 94 + return fmt.Errorf("failed to read session: %w", err) 95 + } 96 + 97 + var session Session 98 + if err := json.Unmarshal(sessionBytes, &session); err != nil { 99 + return fmt.Errorf("failed to unmarshal session: %w", err) 100 + } 101 + 102 + if session.ExpiresAt.Before(time.Now()) { 103 + if err := DeleteSession(session.ID); err != nil { 104 + return err 105 + } 106 + } 107 + } 108 + 109 + return nil 110 + } 111 + 112 + func GetSession(sessionID string, domain string) (Session, error) { 113 + sessionPath := sessionPath(sessionID) 114 + sessionBytes, err := os.ReadFile(sessionPath) 115 + if err != nil { 116 + return Session{}, fmt.Errorf("failed to read session: %w", err) 117 + } 118 + 119 + var session Session 120 + if err := json.Unmarshal(sessionBytes, &session); err != nil { 121 + return Session{}, fmt.Errorf("failed to unmarshal session: %w", err) 122 + } 123 + 124 + if session.Domain != domain { 125 + return Session{}, fmt.Errorf("session not found") 126 + } 127 + 128 + return session, nil 129 + } 130 + 131 + func LoadSession(sessionID string) (Session, error) { 132 + sessionPath := sessionPath(sessionID) 133 + sessionBytes, err := os.ReadFile(sessionPath) 134 + if err != nil { 135 + return Session{}, fmt.Errorf("failed to read session: %w", err) 136 + } 137 + 138 + var session Session 139 + if err := json.Unmarshal(sessionBytes, &session); err != nil { 140 + return Session{}, fmt.Errorf("failed to unmarshal session: %w", err) 141 + } 142 + 143 + return session, nil 144 + } 145 + 146 + func ExtendSession(sessionID string, expiresAt time.Time) error { 147 + session, err := LoadSession(sessionID) 148 + if err != nil { 149 + return fmt.Errorf("failed to load session: %w", err) 150 + } 151 + 152 + session.ExpiresAt = expiresAt 153 + return SaveSession(session) 154 + }
+151
auth/token.go
··· 1 + package auth 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "os" 7 + "path/filepath" 8 + "strings" 9 + "time" 10 + 11 + "github.com/pomdtr/smallweb/utils" 12 + "golang.org/x/crypto/bcrypt" 13 + ) 14 + 15 + type Token struct { 16 + ID string `json:"id"` 17 + Hash []byte `json:"hash"` 18 + Description string `json:"description"` 19 + CreatedAt time.Time `json:"createdAt"` 20 + App string `json:"app"` 21 + } 22 + 23 + func tokenDir() string { 24 + return filepath.Join(utils.DataDir(), "tokens") 25 + } 26 + 27 + func tokenPath(publicID string) string { 28 + return filepath.Join(tokenDir(), fmt.Sprintf("%s.json", publicID)) 29 + } 30 + 31 + // Lengths for the public and secret parts 32 + const publicPartLength = 16 // 16 characters for public part 33 + const secretPartLength = 59 // 43 characters for secret part 34 + 35 + const TokenPrefix = "smallweb_pat" 36 + 37 + func GenerateToken() (string, string, string, error) { 38 + // Generate public and secret parts with Base62 encoding 39 + publicPart, err := utils.GenerateBase62String(publicPartLength) 40 + if err != nil { 41 + return "", "", "", err 42 + } 43 + secretPart, err := utils.GenerateBase62String(secretPartLength) 44 + if err != nil { 45 + return "", "", "", err 46 + } 47 + 48 + // Assemble the token with the given prefix 49 + return fmt.Sprintf("%s_%s_%s", TokenPrefix, publicPart, secretPart), publicPart, secretPart, nil 50 + } 51 + 52 + func GetToken(publicID string) (Token, error) { 53 + tokenBytes, err := os.ReadFile(tokenPath(publicID)) 54 + if err != nil { 55 + return Token{}, err 56 + } 57 + 58 + var token Token 59 + if err := json.Unmarshal(tokenBytes, &token); err != nil { 60 + return Token{}, err 61 + } 62 + 63 + return token, nil 64 + } 65 + 66 + func CreateToken(token Token) error { 67 + tokenBytes, err := json.Marshal(token) 68 + if err != nil { 69 + return err 70 + } 71 + 72 + if err := os.WriteFile(tokenPath(token.ID), tokenBytes, 0600); err != nil { 73 + return err 74 + } 75 + 76 + return nil 77 + } 78 + 79 + func VerifyToken(token string, appname string) error { 80 + public, secret, err := ParseToken(token) 81 + if err != nil { 82 + return err 83 + } 84 + 85 + t, err := GetToken(public) 86 + if err != nil { 87 + return err 88 + } 89 + 90 + if t.App != appname { 91 + return fmt.Errorf("invalid token") 92 + } 93 + 94 + if err := bcrypt.CompareHashAndPassword([]byte(t.Hash), []byte(secret)); err != nil { 95 + return err 96 + } 97 + 98 + return nil 99 + } 100 + 101 + func ParseToken(token string) (string, string, error) { 102 + if !strings.HasPrefix(token, TokenPrefix) { 103 + return "", "", fmt.Errorf("invalid token format") 104 + } 105 + 106 + parts := strings.Split(token, "_") 107 + if len(parts) != 4 { 108 + return "", "", fmt.Errorf("invalid token format") 109 + } 110 + 111 + public, secret := parts[2], parts[3] 112 + if len(public) != publicPartLength || len(secret) != secretPartLength { 113 + return "", "", fmt.Errorf("invalid token format") 114 + } 115 + 116 + return parts[2], parts[3], nil 117 + } 118 + 119 + func ListTokens() ([]Token, error) { 120 + entries, err := os.ReadDir(tokenDir()) 121 + if err != nil && os.IsNotExist(err) { 122 + return nil, nil 123 + } else if err != nil { 124 + return nil, err 125 + } 126 + 127 + tokens := make([]Token, 0) 128 + for _, entry := range entries { 129 + tokenBytes, err := os.ReadFile(filepath.Join(tokenDir(), entry.Name())) 130 + if err != nil { 131 + return nil, err 132 + } 133 + 134 + var token Token 135 + if err := json.Unmarshal(tokenBytes, &token); err != nil { 136 + return nil, err 137 + } 138 + 139 + tokens = append(tokens, token) 140 + } 141 + 142 + return tokens, nil 143 + } 144 + 145 + func DeleteToken(id string) error { 146 + if err := os.Remove(tokenPath(id)); err != nil { 147 + return fmt.Errorf("failed to delete token: %w", err) 148 + } 149 + 150 + return nil 151 + }
-93
cmd/api.go
··· 1 - package cmd 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "io" 8 - "net" 9 - "net/http" 10 - "os" 11 - "strings" 12 - 13 - "github.com/pomdtr/smallweb/api" 14 - "github.com/spf13/cobra" 15 - ) 16 - 17 - func NewCmdAPI() *cobra.Command { 18 - var flags struct { 19 - method string 20 - headers []string 21 - data string 22 - } 23 - 24 - cmd := &cobra.Command{ 25 - Use: "api", 26 - Short: "Interact with the smallweb API", 27 - GroupID: CoreGroupID, 28 - Args: cobra.ExactArgs(1), 29 - RunE: func(cmd *cobra.Command, args []string) error { 30 - var body io.Reader 31 - if flags.data != "" { 32 - body = strings.NewReader(flags.data) 33 - } else if flags.data == "@-" { 34 - body = os.Stdin 35 - } 36 - 37 - // use api unix socket if available 38 - client := &http.Client{ 39 - Transport: &http.Transport{ 40 - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 41 - return net.Dial("unix", api.SocketPath(k.String("domain"))) 42 - }, 43 - }, 44 - } 45 - 46 - req, err := http.NewRequest(flags.method, "http://smallweb"+args[0], body) 47 - if err != nil { 48 - return fmt.Errorf("failed to create request: %w", err) 49 - } 50 - 51 - for _, header := range flags.headers { 52 - parts := strings.SplitN(header, ":", 2) 53 - if len(parts) != 2 { 54 - return fmt.Errorf("invalid header: %s", header) 55 - } 56 - 57 - req.Header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) 58 - } 59 - 60 - resp, err := client.Do(req) 61 - if err != nil { 62 - return fmt.Errorf("failed to send request: %w", err) 63 - } 64 - defer resp.Body.Close() 65 - 66 - if resp.Header.Get("Content-Type") == "application/json" { 67 - var v any 68 - decoder := json.NewDecoder(resp.Body) 69 - if err := decoder.Decode(&v); err != nil { 70 - return fmt.Errorf("failed to decode JSON: %w", err) 71 - } 72 - 73 - encoder := json.NewEncoder(os.Stdout) 74 - encoder.SetEscapeHTML(false) 75 - encoder.SetIndent("", " ") 76 - if err := encoder.Encode(v); err != nil { 77 - return fmt.Errorf("failed to encode JSON: %w", err) 78 - } 79 - 80 - return nil 81 - } 82 - 83 - _, _ = io.Copy(os.Stdout, resp.Body) 84 - return nil 85 - }, 86 - } 87 - 88 - cmd.Flags().StringVarP(&flags.method, "method", "X", "GET", "HTTP method to use") 89 - cmd.Flags().StringArrayVarP(&flags.headers, "header", "H", nil, "HTTP headers to use") 90 - cmd.Flags().StringVarP(&flags.data, "data", "d", "", "Data to send in the request body") 91 - 92 - return cmd 93 - }
+26 -10
cmd/app.go
··· 13 13 "github.com/cli/go-gh/v2/pkg/tableprinter" 14 14 "github.com/mattn/go-isatty" 15 15 "github.com/pomdtr/smallweb/app" 16 + "github.com/pomdtr/smallweb/utils" 16 17 "github.com/spf13/cobra" 17 18 "golang.org/x/term" 18 19 ) ··· 28 29 GroupID: CoreGroupID, 29 30 Args: cobra.ExactArgs(1), 30 31 RunE: func(cmd *cobra.Command, args []string) error { 31 - rootDir := k.String("dir") 32 + rootDir := utils.RootDir() 32 33 appDir := filepath.Join(rootDir, args[0]) 33 34 if _, err := os.Stat(appDir); !os.IsNotExist(err) { 34 35 return fmt.Errorf("directory already exists: %s", appDir) ··· 57 58 Short: "Open an app in the browser", 58 59 GroupID: CoreGroupID, 59 60 Args: cobra.MaximumNArgs(1), 60 - ValidArgsFunction: completeApp(k.String("dir")), 61 + ValidArgsFunction: completeApp(utils.RootDir()), 61 62 RunE: func(cmd *cobra.Command, args []string) error { 62 - rootDir := k.String("dir") 63 + rootDir := utils.RootDir() 63 64 64 65 if len(args) == 0 { 65 66 cwd, err := os.Getwd() ··· 111 112 GroupID: CoreGroupID, 112 113 Aliases: []string{"ls"}, 113 114 RunE: func(cmd *cobra.Command, args []string) error { 114 - rootDir := k.String("dir") 115 + rootDir := utils.RootDir() 115 116 names, err := app.ListApps(rootDir) 116 117 if err != nil { 117 118 return fmt.Errorf("failed to list apps: %w", err) ··· 182 183 Short: "Rename an app", 183 184 GroupID: CoreGroupID, 184 185 Aliases: []string{"move", "mv"}, 185 - ValidArgsFunction: completeApp(k.String("dir")), 186 + ValidArgsFunction: completeApp(utils.RootDir()), 186 187 Args: cobra.ExactArgs(2), 187 188 RunE: func(cmd *cobra.Command, args []string) error { 188 - rootDir := k.String("dir") 189 + rootDir := utils.RootDir() 189 190 src := filepath.Join(rootDir, args[0]) 190 191 dst := filepath.Join(rootDir, args[1]) 191 192 ··· 215 216 Short: "Clone an app", 216 217 GroupID: CoreGroupID, 217 218 Aliases: []string{"cp", "copy", "fork"}, 218 - ValidArgsFunction: completeApp(k.String("dir")), 219 + ValidArgsFunction: completeApp(utils.RootDir()), 219 220 Args: cobra.ExactArgs(2), 220 221 RunE: func(cmd *cobra.Command, args []string) error { 221 - rootDir := k.String("dir") 222 + rootDir := utils.RootDir() 222 223 src := filepath.Join(rootDir, args[0]) 223 224 dst := filepath.Join(rootDir, args[1]) 224 225 ··· 249 250 Short: "Delete an app", 250 251 GroupID: CoreGroupID, 251 252 Aliases: []string{"remove", "rm"}, 252 - ValidArgsFunction: completeApp(k.String("dir")), 253 + ValidArgsFunction: completeApp(utils.RootDir()), 253 254 Args: cobra.ExactArgs(1), 254 255 RunE: func(cmd *cobra.Command, args []string) error { 255 - rootDir := k.String("dir") 256 + rootDir := utils.RootDir() 256 257 p := filepath.Join(rootDir, args[0]) 257 258 if _, err := os.Stat(p); os.IsNotExist(err) { 258 259 return fmt.Errorf("app not found: %s", args[0]) ··· 269 270 270 271 return cmd 271 272 } 273 + 274 + func completeApp(rootDir string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 275 + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 276 + if len(args) > 0 { 277 + return nil, cobra.ShellCompDirectiveDefault 278 + } 279 + 280 + apps, err := app.ListApps(rootDir) 281 + if err != nil { 282 + return nil, cobra.ShellCompDirectiveDefault 283 + } 284 + 285 + return apps, cobra.ShellCompDirectiveDefault 286 + } 287 + }
+1 -2
cmd/config.go
··· 19 19 GroupID: CoreGroupID, 20 20 Args: cobra.MaximumNArgs(1), 21 21 RunE: func(cmd *cobra.Command, args []string) error { 22 - configPath := findConfigPath() 23 - 22 + configPath := filepath.Join(utils.RootDir(), ".smallweb", "config.json") 24 23 if !utils.FileExists(configPath) { 25 24 var config map[string]any 26 25 if err := k.Unmarshal("", &config); err != nil {
-233
cmd/cron.go
··· 1 - package cmd 2 - 3 - import ( 4 - "encoding/json" 5 - "fmt" 6 - "os" 7 - "path/filepath" 8 - "strings" 9 - 10 - "github.com/cli/go-gh/v2/pkg/tableprinter" 11 - "github.com/mattn/go-isatty" 12 - "github.com/pomdtr/smallweb/app" 13 - "github.com/pomdtr/smallweb/worker" 14 - "github.com/spf13/cobra" 15 - "golang.org/x/term" 16 - ) 17 - 18 - func NewCmdCron() *cobra.Command { 19 - cmd := &cobra.Command{ 20 - Use: "cron", 21 - Short: "Manage cron jobs", 22 - GroupID: CoreGroupID, 23 - } 24 - 25 - cmd.AddCommand(NewCmdCronList()) 26 - cmd.AddCommand(NewCmdCronTrigger()) 27 - return cmd 28 - } 29 - 30 - type CronItem struct { 31 - ID string `json:"id"` 32 - App string `json:"app"` 33 - app.CronJob 34 - } 35 - 36 - func ListCronItems(app app.App) ([]CronItem, error) { 37 - var items []CronItem 38 - for _, job := range app.Config.Crons { 39 - items = append(items, CronItem{App: app.Name, ID: fmt.Sprintf("%s:%s", filepath.Base(app.Dir), job.Name), CronJob: job}) 40 - } 41 - 42 - return items, nil 43 - } 44 - 45 - func completeApp(rootDir string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 46 - return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 47 - if len(args) > 0 { 48 - return nil, cobra.ShellCompDirectiveDefault 49 - } 50 - 51 - apps, err := app.ListApps(rootDir) 52 - if err != nil { 53 - return nil, cobra.ShellCompDirectiveDefault 54 - } 55 - 56 - return apps, cobra.ShellCompDirectiveDefault 57 - } 58 - } 59 - 60 - func NewCmdCronList() *cobra.Command { 61 - var flags struct { 62 - json bool 63 - app string 64 - } 65 - 66 - cmd := &cobra.Command{ 67 - Use: "list", 68 - Aliases: []string{"ls"}, 69 - Args: cobra.NoArgs, 70 - Short: "List cron jobs", 71 - RunE: func(cmd *cobra.Command, args []string) error { 72 - rootDir := k.String("dir") 73 - 74 - apps, err := app.ListApps(rootDir) 75 - if err != nil { 76 - return fmt.Errorf("failed to list apps: %w", err) 77 - } 78 - 79 - var crons []CronItem 80 - for _, name := range apps { 81 - if cmd.Flags().Changed("app") && flags.app != name { 82 - continue 83 - } 84 - 85 - app, err := app.LoadApp(filepath.Join(rootDir, name), k.String("domain")) 86 - if err != nil { 87 - return fmt.Errorf("failed to load app: %w", err) 88 - } 89 - 90 - items, err := ListCronItems(app) 91 - if err != nil { 92 - return fmt.Errorf("failed to list cron jobs: %w", err) 93 - } 94 - 95 - crons = append(crons, items...) 96 - } 97 - 98 - if flags.json { 99 - encoder := json.NewEncoder(os.Stdout) 100 - encoder.SetEscapeHTML(false) 101 - if isatty.IsTerminal(os.Stdout.Fd()) { 102 - encoder.SetIndent("", " ") 103 - } 104 - 105 - if err := encoder.Encode(crons); err != nil { 106 - return fmt.Errorf("failed to encode cron jobs: %w", err) 107 - } 108 - return nil 109 - } 110 - 111 - if (len(crons)) == 0 { 112 - cmd.Println("No cron jobs found") 113 - return nil 114 - } 115 - 116 - var printer tableprinter.TablePrinter 117 - if isatty.IsTerminal(os.Stdout.Fd()) { 118 - width, _, err := term.GetSize(int(os.Stdout.Fd())) 119 - if err != nil { 120 - return fmt.Errorf("failed to get terminal size: %w", err) 121 - } 122 - 123 - printer = tableprinter.New(os.Stdout, true, width) 124 - } else { 125 - printer = tableprinter.New(os.Stdout, false, 0) 126 - } 127 - 128 - printer.AddHeader([]string{"ID", "Schedule", "Args", "Description"}) 129 - for _, item := range crons { 130 - printer.AddField(item.ID) 131 - printer.AddField(item.Schedule) 132 - 133 - args, err := json.Marshal(item.Args) 134 - if err != nil { 135 - return fmt.Errorf("failed to marshal args: %w", err) 136 - } 137 - printer.AddField(string(args)) 138 - printer.AddField(item.Description) 139 - 140 - printer.EndRow() 141 - } 142 - 143 - if err := printer.Render(); err != nil { 144 - return fmt.Errorf("failed to render table: %w", err) 145 - } 146 - 147 - return nil 148 - }, 149 - } 150 - 151 - cmd.Flags().StringVar(&flags.app, "app", "", "filter by app") 152 - cmd.Flags().BoolVar(&flags.json, "json", false, "output as json") 153 - cmd.RegisterFlagCompletionFunc("app", completeApp(k.String("dir"))) 154 - 155 - return cmd 156 - } 157 - 158 - func NewCmdCronTrigger() *cobra.Command { 159 - cmd := &cobra.Command{ 160 - Use: "trigger <id>", 161 - Short: "Trigger a cron job", 162 - Args: cobra.ExactArgs(1), 163 - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 164 - rootDir := k.String("dir") 165 - 166 - var completions []string 167 - apps, err := app.ListApps(rootDir) 168 - if err != nil { 169 - return nil, cobra.ShellCompDirectiveDefault 170 - } 171 - 172 - for _, name := range apps { 173 - app, err := app.LoadApp(filepath.Join(rootDir, name), k.String("domain")) 174 - if err != nil { 175 - continue 176 - } 177 - 178 - jobs, err := ListCronItems(app) 179 - if err != nil { 180 - continue 181 - } 182 - 183 - for _, job := range jobs { 184 - completions = append(completions, fmt.Sprintf("%s\t%s", job.ID, job.Description)) 185 - } 186 - } 187 - 188 - return completions, cobra.ShellCompDirectiveDefault 189 - }, 190 - RunE: func(cmd *cobra.Command, args []string) error { 191 - rootDir := k.String("dir") 192 - parts := strings.Split(args[0], ":") 193 - if len(parts) != 2 { 194 - return fmt.Errorf("invalid job name") 195 - } 196 - 197 - appname, jobName := parts[0], parts[1] 198 - app, err := app.LoadApp(filepath.Join(rootDir, appname), k.String("domain")) 199 - if err != nil { 200 - return fmt.Errorf("failed to get app: %w", err) 201 - } 202 - 203 - crons, err := ListCronItems(app) 204 - if err != nil { 205 - return fmt.Errorf("failed to list cron jobs: %w", err) 206 - } 207 - 208 - for _, cron := range crons { 209 - if cron.Name != jobName { 210 - continue 211 - } 212 - 213 - w := worker.NewWorker(app, nil) 214 - command, err := w.Command(cron.Args...) 215 - if err != nil { 216 - return fmt.Errorf("failed to create command: %w", err) 217 - } 218 - 219 - command.Stdout = os.Stdout 220 - command.Stderr = os.Stderr 221 - if err := command.Run(); err != nil { 222 - return fmt.Errorf("failed to run command: %w", err) 223 - } 224 - } 225 - 226 - return fmt.Errorf("could not find job") 227 - 228 - }, 229 - } 230 - 231 - return cmd 232 - 233 - }
+2 -94
cmd/log.go
··· 10 10 "os" 11 11 12 12 "github.com/pomdtr/smallweb/api" 13 + "github.com/pomdtr/smallweb/utils" 13 14 "github.com/spf13/cobra" 14 15 ) 15 16 ··· 22 23 } 23 24 24 25 cmd.AddCommand(NewCmdLogHttp()) 25 - cmd.AddCommand(NewCmdLogCron()) 26 26 cmd.AddCommand(NewCmdLogConsole()) 27 27 28 28 return cmd ··· 137 137 return cmd 138 138 } 139 139 140 - func NewCmdLogCron() *cobra.Command { 141 - var flags struct { 142 - host string 143 - json bool 144 - } 145 - cmd := &cobra.Command{ 146 - Use: "cron", 147 - Short: "Show cron logs", 148 - RunE: func(cmd *cobra.Command, args []string) error { 149 - // use api unix socket if available 150 - client := &http.Client{ 151 - Transport: &http.Transport{ 152 - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 153 - return net.Dial("unix", api.SocketPath(k.String("domain"))) 154 - }, 155 - }, 156 - } 157 - 158 - req, err := http.NewRequest("GET", "http://unix/v0/logs/cron", nil) 159 - if err != nil { 160 - return err 161 - } 162 - 163 - q := req.URL.Query() 164 - if flags.host != "" { 165 - q.Add("host", flags.host) 166 - } 167 - req.URL.RawQuery = q.Encode() 168 - 169 - resp, err := client.Do(req) 170 - if err != nil { 171 - return fmt.Errorf("failed to get logs: %w", err) 172 - } 173 - defer resp.Body.Close() 174 - 175 - if resp.StatusCode != http.StatusOK { 176 - return fmt.Errorf("failed to get logs: %s", resp.Body) 177 - } 178 - 179 - scanner := bufio.NewScanner(resp.Body) 180 - for scanner.Scan() { 181 - if scanner.Err() != nil { 182 - if scanner.Err().Error() == "EOF" { 183 - break 184 - } 185 - 186 - return fmt.Errorf("failed to read logs: %w", scanner.Err()) 187 - } 188 - 189 - if flags.json { 190 - fmt.Println(scanner.Text()) 191 - continue 192 - } 193 - 194 - var log map[string]any 195 - if err := json.Unmarshal(scanner.Bytes(), &log); err != nil { 196 - return fmt.Errorf("failed to parse log: %w", err) 197 - } 198 - 199 - time, ok := log["time"].(string) 200 - if !ok { 201 - return fmt.Errorf("failed to parse time") 202 - } 203 - 204 - id, ok := log["id"].(string) 205 - if !ok { 206 - return fmt.Errorf("failed to parse id") 207 - } 208 - 209 - schedule, ok := log["schedule"].(string) 210 - if !ok { 211 - return fmt.Errorf("failed to parse schedule") 212 - } 213 - 214 - exitCode, ok := log["exit_code"].(int) 215 - if !ok { 216 - return fmt.Errorf("failed to parse exit_code") 217 - } 218 - 219 - fmt.Printf("%s %s %s %d\n", time, id, schedule, exitCode) 220 - } 221 - 222 - return nil 223 - }, 224 - } 225 - 226 - cmd.Flags().StringVar(&flags.host, "host", "", "filter logs by host") 227 - cmd.Flags().BoolVar(&flags.json, "json", false, "output logs in JSON format") 228 - 229 - return cmd 230 - } 231 - 232 140 func NewCmdLogConsole() *cobra.Command { 233 141 var flags struct { 234 142 json bool ··· 312 220 313 221 cmd.Flags().BoolVar(&flags.json, "json", false, "output logs in JSON format") 314 222 cmd.Flags().StringVar(&flags.app, "app", "", "filter logs by app") 315 - cmd.RegisterFlagCompletionFunc("app", completeApp(k.String("dir"))) 223 + cmd.RegisterFlagCompletionFunc("app", completeApp(utils.RootDir())) 316 224 317 225 return cmd 318 226 }
+2 -24
cmd/root.go
··· 27 27 k = koanf.New(".") 28 28 ) 29 29 30 - func findConfigPath() string { 31 - if config, ok := os.LookupEnv("SMALLWEB_CONFIG"); ok { 32 - return config 33 - } 34 - 35 - var configDir string 36 - if configHome, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok { 37 - configDir = filepath.Join(configHome, "smallweb") 38 - } else { 39 - configDir = filepath.Join(os.Getenv("HOME"), ".config", "smallweb") 40 - } 41 - 42 - if utils.FileExists(filepath.Join(configDir, "config.jsonc")) { 43 - return filepath.Join(configDir, "config.jsonc") 44 - } 45 - 46 - return filepath.Join(configDir, "config.json") 47 - } 48 - 49 30 func NewCmdRoot(version string, changelog string) *cobra.Command { 50 31 defaultProvider := confmap.Provider(map[string]interface{}{ 51 32 "addr": ":7777", ··· 65 46 return strings.Replace(strings.ToLower(key), "_", ".", -1) 66 47 }) 67 48 68 - configPath := findConfigPath() 49 + rootDir := utils.RootDir() 50 + configPath := filepath.Join(rootDir, ".smallweb", "config.json") 69 51 fileProvider := file.Provider(configPath) 70 52 fileProvider.Watch(func(event interface{}, err error) { 71 53 k = koanf.New(".") 72 54 k.Load(defaultProvider, nil) 73 55 k.Load(fileProvider, utils.ConfigParser()) 74 56 k.Load(envProvider, nil) 75 - k.Set("dir", utils.ExpandTilde(k.String("dir"))) 76 57 }) 77 58 78 59 k.Load(defaultProvider, nil) 79 60 k.Load(fileProvider, utils.ConfigParser()) 80 61 k.Load(envProvider, nil) 81 - k.Set("dir", utils.ExpandTilde(k.String("dir"))) 82 62 83 63 cmd := &cobra.Command{ 84 64 Use: "smallweb", ··· 95 75 cmd.AddCommand(NewCmdRun()) 96 76 cmd.AddCommand(NewCmdDocs()) 97 77 cmd.AddCommand(NewCmdTunnel()) 98 - cmd.AddCommand(NewCmdCron()) 99 78 cmd.AddCommand(NewCmdUpgrade()) 100 79 cmd.AddCommand(NewCmdToken()) 101 80 cmd.AddCommand(NewCmdUp()) 102 81 cmd.AddCommand(NewCmdService()) 103 82 cmd.AddCommand(NewCmdConfig()) 104 - cmd.AddCommand(NewCmdAPI()) 105 83 cmd.AddCommand(NewCmdLog()) 106 84 cmd.AddCommand(NewCmdCreate()) 107 85 cmd.AddCommand(NewCmdOpen())
+98 -2
cmd/run.go
··· 1 1 package cmd 2 2 3 3 import ( 4 + "context" 5 + "encoding/json" 4 6 "fmt" 7 + "io" 8 + "net" 9 + "net/http" 5 10 "os" 6 11 "path/filepath" 7 12 "strings" 8 13 14 + "github.com/pomdtr/smallweb/api" 9 15 "github.com/pomdtr/smallweb/app" 16 + "github.com/pomdtr/smallweb/utils" 10 17 "github.com/pomdtr/smallweb/worker" 11 18 "github.com/spf13/cobra" 12 19 ) ··· 18 25 GroupID: CoreGroupID, 19 26 DisableFlagParsing: true, 20 27 SilenceErrors: true, 21 - ValidArgsFunction: completeApp(k.String("dir")), 28 + ValidArgsFunction: completeApp(utils.RootDir()), 22 29 RunE: func(cmd *cobra.Command, args []string) error { 23 30 if len(args) == 0 { 24 31 return cmd.Help() ··· 28 35 return cmd.Help() 29 36 } 30 37 31 - rootDir := k.String("dir") 38 + rootDir := utils.RootDir() 32 39 app, err := app.LoadApp(filepath.Join(rootDir, args[0]), k.String("domain")) 33 40 if err != nil { 34 41 return fmt.Errorf("failed to get app: %w", err) 35 42 } 36 43 44 + if app.Entrypoint() == "smallweb:api" { 45 + apiCmd := NewCmdAPI() 46 + apiCmd.SetArgs(args[1:]) 47 + 48 + apiCmd.SetIn(os.Stdin) 49 + apiCmd.SetOut(os.Stdout) 50 + apiCmd.SetErr(os.Stderr) 51 + 52 + return apiCmd.Execute() 53 + } 54 + 37 55 if strings.HasPrefix(app.Config.Entrypoint, "smallweb:") { 38 56 return fmt.Errorf("smallweb built-in apps cannot be run") 39 57 } ··· 54 72 55 73 return cmd 56 74 } 75 + 76 + func NewCmdAPI() *cobra.Command { 77 + var flags struct { 78 + method string 79 + headers []string 80 + data string 81 + } 82 + 83 + cmd := &cobra.Command{ 84 + Use: "api", 85 + Short: "Interact with the smallweb API", 86 + SilenceUsage: true, 87 + Args: cobra.ExactArgs(1), 88 + RunE: func(cmd *cobra.Command, args []string) error { 89 + var body io.Reader 90 + if flags.data != "" { 91 + body = strings.NewReader(flags.data) 92 + } else if flags.data == "@-" { 93 + body = os.Stdin 94 + } 95 + 96 + // use api unix socket if available 97 + client := &http.Client{ 98 + Transport: &http.Transport{ 99 + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { 100 + return net.Dial("unix", api.SocketPath(k.String("domain"))) 101 + }, 102 + }, 103 + } 104 + 105 + req, err := http.NewRequest(flags.method, "http://smallweb"+args[0], body) 106 + if err != nil { 107 + return fmt.Errorf("failed to create request: %w", err) 108 + } 109 + 110 + for _, header := range flags.headers { 111 + parts := strings.SplitN(header, ":", 2) 112 + if len(parts) != 2 { 113 + return fmt.Errorf("invalid header: %s", header) 114 + } 115 + 116 + req.Header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) 117 + } 118 + 119 + resp, err := client.Do(req) 120 + if err != nil { 121 + return fmt.Errorf("failed to send request: %w", err) 122 + } 123 + defer resp.Body.Close() 124 + 125 + if resp.Header.Get("Content-Type") == "application/json" { 126 + var v any 127 + decoder := json.NewDecoder(resp.Body) 128 + if err := decoder.Decode(&v); err != nil { 129 + return fmt.Errorf("failed to decode JSON: %w", err) 130 + } 131 + 132 + encoder := json.NewEncoder(os.Stdout) 133 + encoder.SetEscapeHTML(false) 134 + encoder.SetIndent("", " ") 135 + if err := encoder.Encode(v); err != nil { 136 + return fmt.Errorf("failed to encode JSON: %w", err) 137 + } 138 + 139 + return nil 140 + } 141 + 142 + _, _ = io.Copy(os.Stdout, resp.Body) 143 + return nil 144 + }, 145 + } 146 + 147 + cmd.Flags().StringVarP(&flags.method, "method", "X", "GET", "HTTP method to use") 148 + cmd.Flags().StringArrayVarP(&flags.headers, "header", "H", nil, "HTTP headers to use") 149 + cmd.Flags().StringVarP(&flags.data, "data", "d", "", "Data to send in the request body") 150 + 151 + return cmd 152 + }
+14 -47
cmd/token.go
··· 5 5 "fmt" 6 6 "os" 7 7 "path/filepath" 8 - "strings" 9 8 "time" 10 9 11 10 "github.com/adrg/xdg" 12 11 "github.com/cli/go-gh/v2/pkg/tableprinter" 13 12 "github.com/mattn/go-isatty" 14 - "github.com/pomdtr/smallweb/database" 13 + "github.com/pomdtr/smallweb/auth" 15 14 "github.com/pomdtr/smallweb/utils" 16 15 "github.com/spf13/cobra" 17 16 "golang.org/x/crypto/bcrypt" ··· 42 41 func NewCmdTokenCreate() *cobra.Command { 43 42 var flags struct { 44 43 description string 45 - admin bool 46 - app []string 44 + app string 47 45 } 48 46 49 47 cmd := &cobra.Command{ ··· 51 49 Aliases: []string{"add", "new"}, 52 50 Short: "Create a new token", 53 51 Args: cobra.NoArgs, 54 - PreRunE: func(cmd *cobra.Command, args []string) error { 55 - if len(flags.app) == 0 && !flags.admin { 56 - return fmt.Errorf("either --admin or --app must be specified") 57 - } 58 - 59 - return nil 60 - }, 61 52 RunE: func(cmd *cobra.Command, args []string) error { 62 - db, err := database.OpenDB(filepath.Join(DataDir(), "smallweb.db")) 63 - if err != nil { 64 - fmt.Println("failed to open database:", err) 65 - return nil 66 - } 67 - 68 - value, public, secret, err := database.GenerateToken() 53 + value, public, secret, err := auth.GenerateToken() 69 54 if err != nil { 70 55 return fmt.Errorf("failed to generate token: %v", err) 71 56 } ··· 75 60 return fmt.Errorf("failed to hash secret: %v", err) 76 61 } 77 62 78 - token := database.Token{ 63 + token := auth.Token{ 79 64 ID: public, 80 65 Description: flags.description, 81 66 Hash: hash, 82 67 CreatedAt: time.Now(), 83 - Admin: flags.admin, 84 - Apps: flags.app, 68 + App: flags.app, 85 69 } 86 70 87 - if err := database.InsertToken(db, token); err != nil { 71 + if err := auth.CreateToken(token); err != nil { 88 72 return fmt.Errorf("failed to insert token: %v", err) 89 73 } 90 74 ··· 100 84 101 85 cmd.Flags().StringVarP(&flags.description, "description", "d", "", "description of the token") 102 86 cmd.MarkFlagRequired("description") 103 - cmd.Flags().BoolVar(&flags.admin, "admin", false, "admin token") 104 - cmd.Flags().StringSliceVarP(&flags.app, "app", "a", nil, "app token") 87 + cmd.Flags().StringVarP(&flags.app, "app", "a", "", "app token") 88 + cmd.MarkFlagRequired("app") 105 89 cmd.RegisterFlagCompletionFunc("app", completeApp(utils.ExpandTilde("~/.smallweb/apps"))) 106 90 cmd.MarkFlagsMutuallyExclusive("admin", "app") 107 91 ··· 119 103 Aliases: []string{"ls"}, 120 104 Args: cobra.NoArgs, 121 105 RunE: func(cmd *cobra.Command, args []string) error { 122 - db, err := database.OpenDB(filepath.Join(DataDir(), "smallweb.db")) 123 - if err != nil { 124 - fmt.Println("failed to open database:", err) 125 - return nil 126 - } 127 - 128 - tokens, err := database.ListTokens(db) 106 + tokens, err := auth.ListTokens() 129 107 if err != nil { 130 108 return fmt.Errorf("failed to list tokens: %v", err) 131 109 } ··· 162 140 printer = tableprinter.New(os.Stdout, false, 0) 163 141 } 164 142 165 - printer.AddHeader([]string{"ID", "Description", "Admin", "Apps", "Creation Time"}) 143 + printer.AddHeader([]string{"ID", "Description", "App", "Creation Time"}) 166 144 for _, token := range tokens { 167 145 printer.AddField(token.ID) 168 146 description := token.Description ··· 170 148 description = "N/A" 171 149 } 172 150 printer.AddField(description) 173 - printer.AddField(fmt.Sprintf("%t", token.Admin)) 174 - if token.Admin { 151 + if token.App == "" { 175 152 printer.AddField("<all>") 176 153 } else { 177 - printer.AddField(strings.Join(token.Apps, ", ")) 154 + printer.AddField(token.App) 178 155 } 179 156 printer.AddField(token.CreatedAt.Format(time.RFC3339)) 180 157 printer.EndRow() ··· 195 172 Args: cobra.ArbitraryArgs, 196 173 Aliases: []string{"remove", "rm"}, 197 174 ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 198 - db, err := database.OpenDB(filepath.Join(DataDir(), "smallweb.db")) 199 - if err != nil { 200 - fmt.Println("failed to open database:", err) 201 - return nil, cobra.ShellCompDirectiveError 202 - } 203 - 204 - tokens, err := database.ListTokens(db) 175 + tokens, err := auth.ListTokens() 205 176 if err != nil { 206 177 return nil, cobra.ShellCompDirectiveError 207 178 } ··· 214 185 return completions, cobra.ShellCompDirectiveNoFileComp 215 186 }, 216 187 RunE: func(cmd *cobra.Command, args []string) error { 217 - db, err := database.OpenDB(filepath.Join(DataDir(), "smallweb.db")) 218 - if err != nil { 219 - return fmt.Errorf("failed to open database: %v", err) 220 - } 221 188 for _, arg := range args { 222 - if err := database.DeleteToken(db, arg); err != nil { 189 + if err := auth.DeleteToken(arg); err != nil { 223 190 return fmt.Errorf("failed to delete token: %v", err) 224 191 } 225 192
+4 -10
cmd/tunnel.go
··· 11 11 12 12 "github.com/pomdtr/smallweb/api" 13 13 "github.com/pomdtr/smallweb/app" 14 - "github.com/pomdtr/smallweb/database" 14 + "github.com/pomdtr/smallweb/utils" 15 15 "github.com/spf13/cobra" 16 16 ) 17 17 ··· 21 21 Short: "Start a tunnel to a remote server (powered by localhost.run)", 22 22 GroupID: CoreGroupID, 23 23 Args: cobra.ExactArgs(1), 24 - ValidArgsFunction: completeApp(k.String("dir")), 24 + ValidArgsFunction: completeApp(utils.RootDir()), 25 25 RunE: func(cmd *cobra.Command, args []string) error { 26 - db, err := database.OpenDB(filepath.Join(DataDir(), "smallweb.db")) 27 - if err != nil { 28 - return fmt.Errorf("failed to open database: %v", err) 29 - } 30 - 31 26 appHandler := AppHandler{ 32 - db: db, 33 - apiServer: api.NewHandler(k, nil, nil, nil), 27 + apiServer: api.NewHandler(utils.RootDir(), k.String("domain"), nil, nil), 34 28 logger: slog.New(slog.NewJSONHandler(os.Stderr, nil)), 35 29 } 36 30 37 31 server := http.Server{ 38 32 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 - rootDir := k.String("dir") 33 + rootDir := utils.RootDir() 40 34 app, err := app.LoadApp(filepath.Join(rootDir, args[0]), k.String("domain")) 41 35 if err != nil { 42 36 w.WriteHeader(http.StatusNotFound)
+9 -116
cmd/up.go
··· 1 1 package cmd 2 2 3 3 import ( 4 - "context" 5 4 "crypto/tls" 6 - "database/sql" 7 5 "fmt" 8 6 "log" 9 7 "log/slog" 10 8 "net" 11 9 "net/http" 12 10 "os" 13 - "os/exec" 14 11 "os/signal" 15 12 "path" 16 13 "path/filepath" 17 14 "strings" 18 - "time" 19 15 20 16 _ "embed" 21 17 ··· 23 19 "github.com/pomdtr/smallweb/api" 24 20 "github.com/pomdtr/smallweb/app" 25 21 "github.com/pomdtr/smallweb/auth" 26 - "github.com/pomdtr/smallweb/database" 27 22 28 23 "github.com/pomdtr/smallweb/utils" 29 24 "github.com/pomdtr/smallweb/worker" 30 - "github.com/robfig/cron/v3" 31 25 "github.com/spf13/cobra" 32 26 33 27 esbuild "github.com/evanw/esbuild/pkg/api" ··· 45 39 return fmt.Errorf("domain cannot be empty") 46 40 } 47 41 48 - db, err := database.OpenDB(filepath.Join(DataDir(), "smallweb.db")) 49 - if err != nil { 50 - return fmt.Errorf("failed to open database: %v", err) 51 - } 52 - 53 42 httpWriter := utils.NewMultiWriter() 54 - cronWriter := utils.NewMultiWriter() 55 43 consoleWriter := utils.NewMultiWriter() 56 44 57 45 consoleLogger := slog.New(slog.NewJSONHandler(consoleWriter, nil)) 58 46 59 - apiHandler := api.NewHandler(k, httpWriter, cronWriter, consoleWriter) 60 - appHandler := &AppHandler{apiServer: apiHandler, db: db, logger: consoleLogger} 47 + apiHandler := api.NewHandler(utils.RootDir(), k.String("domain"), httpWriter, consoleWriter) 48 + appHandler := &AppHandler{apiServer: apiHandler, logger: consoleLogger} 61 49 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 62 - rootDir := k.String("dir") 50 + rootDir := utils.RootDir() 63 51 64 52 if r.Host == k.String("domain") { 65 53 // if we are on the apex domain and www exists, redirect to www ··· 100 88 appHandler.ServeApp(w, r, a) 101 89 }) 102 90 103 - parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) 104 - c := cron.New(cron.WithParser(parser)) 105 - cronLogger := slog.New(slog.NewJSONHandler(cronWriter, nil)) 106 - c.AddFunc("* * * * *", func() { 107 - rounded := time.Now().Truncate(time.Minute) 108 - rootDir := k.String("dir") 109 - apps, err := app.ListApps(rootDir) 110 - if err != nil { 111 - fmt.Println(err) 112 - } 113 - 114 - for _, name := range apps { 115 - a, err := app.LoadApp(filepath.Join(rootDir, name), k.String("domain")) 116 - if err != nil { 117 - fmt.Println(err) 118 - continue 119 - } 120 - 121 - for _, job := range a.Config.Crons { 122 - sched, err := parser.Parse(job.Schedule) 123 - if err != nil { 124 - fmt.Println(err) 125 - continue 126 - } 127 - 128 - if sched.Next(rounded.Add(-1*time.Second)) != rounded { 129 - continue 130 - } 131 - 132 - wk := worker.NewWorker(a, consoleLogger) 133 - 134 - command, err := wk.Command(job.Args...) 135 - if err != nil { 136 - fmt.Println(err) 137 - continue 138 - } 139 - command.Stdout = os.Stdout 140 - command.Stderr = os.Stderr 141 - 142 - t1 := time.Now() 143 - var exitCode int 144 - if err := command.Run(); err != nil { 145 - if exitError, ok := err.(*exec.ExitError); ok { 146 - exitCode = exitError.ExitCode() 147 - } 148 - } 149 - duration := time.Since(t1) 150 - 151 - cronLogger.LogAttrs( 152 - context.Background(), 153 - slog.LevelInfo, 154 - fmt.Sprintf("Exit Code: %d", exitCode), 155 - slog.String("type", "cron"), 156 - slog.String("id", fmt.Sprintf("%s:%s", a.Name, job.Name)), 157 - slog.String("app", a.Name), 158 - slog.String("job", job.Name), 159 - slog.String("schedule", job.Schedule), 160 - slog.Any("args", job.Args), 161 - slog.Int("exit_code", exitCode), 162 - slog.Int64("duration", duration.Milliseconds()), 163 - ) 164 - } 165 - } 166 - }) 167 - 168 - go c.Start() 169 - 170 - fmt.Fprintf(os.Stderr, "Serving *.%s from %s on %s\n", k.String("domain"), k.String("dir"), k.String("addr")) 91 + fmt.Fprintf(os.Stderr, "Serving *.%s from %s on %s\n", k.String("domain"), utils.RootDir(), k.String("addr")) 171 92 httpLogger := utils.NewLogger(httpWriter) 172 93 server := http.Server{ 173 94 Handler: httpLogger.Middleware(handler), ··· 180 101 181 102 go server.Serve(ln) 182 103 183 - // start api server on unix socket 184 - apiServer := http.Server{ 185 - Handler: apiHandler, 186 - } 187 - 188 - go func() { 189 - socketPath := api.SocketPath(k.String("domain")) 190 - if err := os.MkdirAll(filepath.Dir(socketPath), 0755); err != nil { 191 - log.Fatal(err) 192 - } 193 - 194 - if err := os.Remove(socketPath); err != nil && !os.IsNotExist(err) { 195 - log.Fatal(err) 196 - } 197 - defer os.Remove(socketPath) 198 - 199 - listener, err := net.Listen("unix", socketPath) 200 - if err != nil { 201 - log.Fatal(err) 202 - } 203 - 204 - if err := apiServer.Serve(listener); err != nil && err != http.ErrServerClosed { 205 - log.Fatal(err) 206 - } 207 - }() 208 - 209 104 // sigint handling 210 105 sigint := make(chan os.Signal, 1) 211 106 signal.Notify(sigint, os.Interrupt) 212 107 <-sigint 213 108 214 109 log.Println("Shutting down server...") 215 - apiServer.Shutdown(context.Background()) 216 - c.Stop() 110 + server.Close() 217 111 return nil 218 112 }, 219 113 } ··· 258 152 259 153 type AppHandler struct { 260 154 apiServer http.Handler 261 - db *sql.DB 262 155 logger *slog.Logger 263 156 } 264 157 ··· 333 226 return 334 227 } 335 228 }) 336 - } else if !strings.HasPrefix(a.Entrypoint(), "smallweb:") { 337 - handler = worker.NewWorker(a, me.logger) 338 - } else { 229 + } else if strings.HasPrefix(a.Entrypoint(), "smallweb:") { 339 230 http.Error(w, "invalid entrypoint", http.StatusInternalServerError) 340 231 return 232 + } else { 233 + handler = worker.NewWorker(a, me.logger) 341 234 } 342 235 343 236 isPrivateRoute := a.Config.Private ··· 356 249 } 357 250 358 251 if isPrivateRoute || strings.HasPrefix(r.URL.Path, "/_auth") { 359 - authMiddleware := auth.Middleware(me.db, k.String("auth"), k.String("email"), a.Name) 252 + authMiddleware := auth.Middleware(k.String("auth"), k.String("email"), a.Name) 360 253 handler = authMiddleware(handler) 361 254 } 362 255
-23
database/migrations/20240929092753_init.sql
··· 1 - -- +goose Up 2 - -- +goose StatementBegin 3 - CREATE TABLE IF NOT EXISTS tokens ( 4 - id TEXT PRIMARY KEY, 5 - hash TEXT NOT NULL, 6 - description TEXT, 7 - createdAt TIMESTAMP NOT NULL 8 - ); 9 - 10 - CREATE TABLE IF NOT EXISTS sessions ( 11 - id TEXT PRIMARY KEY, 12 - email TEXT NOT NULL, 13 - domain TEXT NOT NULL, 14 - createdAt TIMESTAMP NOT NULL, 15 - expiresAt TIMESTAMP NOT NULL 16 - ); 17 - -- +goose StatementEnd 18 - 19 - -- +goose Down 20 - -- +goose StatementBegin 21 - DROP TABLE IF EXISTS tokens; 22 - DROP TABLE IF EXISTS sessions; 23 - -- +goose StatementEnd
-17
database/migrations/20240929093129_add_column_scopes.sql
··· 1 - -- +goose Up 2 - -- +goose StatementBegin 3 - ALTER TABLE tokens ADD COLUMN admin BOOLEAN NOT NULL DEFAULT FALSE; 4 - UPDATE tokens SET admin = TRUE; 5 - CREATE TABLE IF NOT EXISTS token_apps ( 6 - token_id TEXT NOT NULL, 7 - app_name TEXT NOT NULL, 8 - PRIMARY KEY (token_id, app_name), 9 - FOREIGN KEY (token_id) REFERENCES tokens(id) ON DELETE CASCADE 10 - ) 11 - -- +goose StatementEnd 12 - 13 - -- +goose Down 14 - -- +goose StatementBegin 15 - ALTER TABLE tokens DROP COLUMN all_apps_accesss; 16 - DROP TABLE IF EXISTS token_apps; 17 - -- +goose StatementEnd
-35
database/session.go
··· 1 - package database 2 - 3 - import ( 4 - "database/sql" 5 - "time" 6 - ) 7 - 8 - type Session struct { 9 - ID string `json:"id"` 10 - Email string `json:"email"` 11 - Domain string `json:"domain"` 12 - CreatedAt time.Time `json:"createdAt"` 13 - ExpiresAt time.Time `json:"expiresAt"` 14 - } 15 - 16 - func InsertSession(db *sql.DB, session *Session) error { 17 - _, err := db.Exec("INSERT INTO sessions (id, email, domain, createdAt, expiresAt) VALUES (?, ?, ?, ?, ?)", session.ID, session.Email, session.Domain, session.CreatedAt, session.ExpiresAt) 18 - return err 19 - } 20 - 21 - func GetSession(db *sql.DB, id string) (*Session, error) { 22 - session := &Session{} 23 - err := db.QueryRow("SELECT id, email, domain, createdAt, expiresAt FROM sessions WHERE id = ?", id).Scan(&session.ID, &session.Email, &session.Domain, &session.CreatedAt, &session.ExpiresAt) 24 - return session, err 25 - } 26 - 27 - func UpdateSession(db *sql.DB, session *Session) error { 28 - _, err := db.Exec("UPDATE sessions SET email = ?, domain = ?, createdAt = ?, expiresAt = ? WHERE id = ?", session.Email, session.Domain, session.CreatedAt, session.ExpiresAt, session.ID) 29 - return err 30 - } 31 - 32 - func DeleteSession(db *sql.DB, id string) error { 33 - _, err := db.Exec("DELETE FROM sessions WHERE id = ?", id) 34 - return err 35 - }
-38
database/sqlite.go
··· 1 - package database 2 - 3 - import ( 4 - "database/sql" 5 - "embed" 6 - "fmt" 7 - "os" 8 - "path/filepath" 9 - 10 - "github.com/pressly/goose/v3" 11 - _ "modernc.org/sqlite" 12 - ) 13 - 14 - //go:embed migrations/*.sql 15 - var fs embed.FS 16 - 17 - func OpenDB(dbPath string) (*sql.DB, error) { 18 - if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { 19 - return nil, fmt.Errorf("failed to create directory: %v", err) 20 - } 21 - 22 - db, err := sql.Open("sqlite", fmt.Sprintf("file:%s", dbPath)) 23 - if err != nil { 24 - return nil, fmt.Errorf("failed to open database: %v", err) 25 - } 26 - 27 - goose.SetLogger(goose.NopLogger()) 28 - goose.SetBaseFS(fs) 29 - if err := goose.SetDialect("sqlite3"); err != nil { 30 - return nil, fmt.Errorf("failed to set dialect: %v", err) 31 - } 32 - 33 - if err := goose.Up(db, "migrations"); err != nil { 34 - return nil, fmt.Errorf("failed to run migrations: %v", err) 35 - } 36 - 37 - return db, nil 38 - }
-189
database/token.go
··· 1 - package database 2 - 3 - import ( 4 - "database/sql" 5 - "fmt" 6 - "strings" 7 - "time" 8 - 9 - "github.com/pomdtr/smallweb/utils" 10 - ) 11 - 12 - type Token struct { 13 - ID string `json:"id"` 14 - Hash []byte `json:"hash"` 15 - Description string `json:"description"` 16 - CreatedAt time.Time `json:"createdAt"` 17 - Admin bool `json:"admin"` 18 - Apps []string `json:"apps"` 19 - } 20 - 21 - type TokenApp struct { 22 - TokenID string `json:"tokenID"` 23 - AppName string `json:"appName"` 24 - } 25 - 26 - // Lengths for the public and secret parts 27 - const publicPartLength = 16 // 16 characters for public part 28 - const secretPartLength = 59 // 43 characters for secret part 29 - 30 - const TokenPrefix = "smallweb_pat" 31 - 32 - func GenerateToken() (string, string, string, error) { 33 - // Generate public and secret parts with Base62 encoding 34 - publicPart, err := utils.GenerateBase62String(publicPartLength) 35 - if err != nil { 36 - return "", "", "", err 37 - } 38 - secretPart, err := utils.GenerateBase62String(secretPartLength) 39 - if err != nil { 40 - return "", "", "", err 41 - } 42 - 43 - // Assemble the token with the given prefix 44 - return fmt.Sprintf("%s_%s_%s", TokenPrefix, publicPart, secretPart), publicPart, secretPart, nil 45 - } 46 - 47 - func ParseToken(token string) (string, string, error) { 48 - if !strings.HasPrefix(token, TokenPrefix) { 49 - return "", "", fmt.Errorf("invalid token format") 50 - } 51 - 52 - parts := strings.Split(token, "_") 53 - if len(parts) != 4 { 54 - return "", "", fmt.Errorf("invalid token format") 55 - } 56 - 57 - public, secret := parts[2], parts[3] 58 - if len(public) != publicPartLength || len(secret) != secretPartLength { 59 - return "", "", fmt.Errorf("invalid token format") 60 - } 61 - 62 - return parts[2], parts[3], nil 63 - } 64 - 65 - func InsertToken(db *sql.DB, token Token) error { 66 - if token.Admin && len(token.Apps) > 0 { 67 - return fmt.Errorf("admin tokens cannot have apps") 68 - } 69 - 70 - if !token.Admin && len(token.Apps) == 0 { 71 - return fmt.Errorf("non-admin tokens must have apps") 72 - } 73 - 74 - tx, err := db.Begin() 75 - if err != nil { 76 - return err 77 - } 78 - 79 - _, err = tx.Exec("INSERT INTO tokens (id, hash, description, createdAt, admin) VALUES (?, ?, ?, ?, ?)", token.ID, token.Hash, token.Description, token.CreatedAt, token.Admin) 80 - if err != nil { 81 - tx.Rollback() 82 - return err 83 - } 84 - 85 - for _, app := range token.Apps { 86 - _, err = tx.Exec("INSERT INTO token_apps (token_id, app_name) VALUES (?, ?)", token.ID, app) 87 - if err != nil { 88 - tx.Rollback() 89 - return err 90 - } 91 - } 92 - 93 - err = tx.Commit() 94 - if err != nil { 95 - tx.Rollback() 96 - return err 97 - } 98 - 99 - return nil 100 - } 101 - 102 - func GetToken(db *sql.DB, id string) (Token, error) { 103 - token := Token{ 104 - Apps: []string{}, 105 - } 106 - err := db.QueryRow("SELECT id, hash, description, createdAt, admin FROM tokens WHERE id = ?", id).Scan(&token.ID, &token.Hash, &token.Description, &token.CreatedAt, &token.Admin) 107 - if err != nil { 108 - return token, err 109 - } 110 - 111 - rows, err := db.Query("SELECT app_name FROM token_apps WHERE token_id = ?", id) 112 - if err != nil { 113 - return token, err 114 - } 115 - defer rows.Close() 116 - 117 - for rows.Next() { 118 - var app string 119 - err := rows.Scan(&app) 120 - if err != nil { 121 - return token, err 122 - } 123 - 124 - token.Apps = append(token.Apps, app) 125 - } 126 - 127 - return token, err 128 - } 129 - 130 - func ListTokenApps(db *sql.DB) ([]TokenApp, error) { 131 - rows, err := db.Query("SELECT token_id, app_name FROM token_apps") 132 - if err != nil { 133 - return nil, err 134 - } 135 - 136 - defer rows.Close() 137 - 138 - apps := []TokenApp{} 139 - for rows.Next() { 140 - app := TokenApp{} 141 - err := rows.Scan(&app.TokenID, &app.AppName) 142 - if err != nil { 143 - return nil, err 144 - } 145 - 146 - apps = append(apps, app) 147 - } 148 - 149 - return apps, nil 150 - } 151 - 152 - func ListTokens(db *sql.DB) ([]Token, error) { 153 - tokenApps, err := ListTokenApps(db) 154 - if err != nil { 155 - return nil, err 156 - } 157 - 158 - rows, err := db.Query("SELECT id, hash, description, createdAt, admin FROM tokens") 159 - if err != nil { 160 - return nil, err 161 - } 162 - defer rows.Close() 163 - 164 - tokens := []Token{} 165 - for rows.Next() { 166 - token := Token{ 167 - Apps: []string{}, 168 - } 169 - err := rows.Scan(&token.ID, &token.Hash, &token.Description, &token.CreatedAt, &token.Admin) 170 - if err != nil { 171 - return nil, err 172 - } 173 - 174 - for _, app := range tokenApps { 175 - if app.TokenID == token.ID { 176 - token.Apps = append(token.Apps, app.AppName) 177 - } 178 - } 179 - 180 - tokens = append(tokens, token) 181 - } 182 - 183 - return tokens, nil 184 - } 185 - 186 - func DeleteToken(db *sql.DB, id string) error { 187 - _, err := db.Exec("DELETE FROM tokens WHERE id = ?", id) 188 - return err 189 - }
+1 -19
go.mod
··· 22 22 github.com/mattn/go-isatty v0.0.20 23 23 github.com/oapi-codegen/oapi-codegen/v2 v2.4.0 24 24 github.com/oapi-codegen/runtime v1.1.1 25 - github.com/robfig/cron/v3 v3.0.1 26 25 github.com/spf13/cobra v1.8.1 27 26 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a 28 27 golang.org/x/crypto v0.27.0 29 28 golang.org/x/net v0.29.0 30 29 golang.org/x/oauth2 v0.23.0 31 30 golang.org/x/term v0.24.0 32 - modernc.org/sqlite v1.33.0 33 31 ) 34 32 35 - require ( 36 - github.com/mssola/user_agent v0.6.0 37 - github.com/pressly/goose/v3 v3.22.1 38 - ) 33 + require github.com/mssola/user_agent v0.6.0 39 34 40 35 require ( 41 36 github.com/alecthomas/chroma/v2 v2.14.0 // indirect ··· 49 44 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 50 45 github.com/dlclark/regexp2 v1.11.0 // indirect 51 46 github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect 52 - github.com/dustin/go-humanize v1.0.1 // indirect 53 47 github.com/fsnotify/fsnotify v1.7.0 // indirect 54 48 github.com/go-openapi/jsonpointer v0.21.0 // indirect 55 49 github.com/go-openapi/swag v0.23.0 // indirect 56 50 github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect 57 - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect 58 51 github.com/google/uuid v1.6.0 // indirect 59 52 github.com/gorilla/css v1.0.1 // indirect 60 - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 61 53 github.com/inconshreveable/mousetrap v1.1.0 // indirect 62 54 github.com/invopop/yaml v0.3.1 // indirect 63 55 github.com/josharian/intern v1.0.0 // indirect ··· 65 57 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 66 58 github.com/mailru/easyjson v0.7.7 // indirect 67 59 github.com/mattn/go-runewidth v0.0.16 // indirect 68 - github.com/mfridman/interpolate v0.0.2 // indirect 69 60 github.com/microcosm-cc/bluemonday v1.0.27 // indirect 70 61 github.com/mitchellh/copystructure v1.2.0 // indirect 71 62 github.com/mitchellh/reflectwalk v1.0.2 // indirect 72 63 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 73 64 github.com/muesli/reflow v0.3.0 // indirect 74 65 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect 75 - github.com/ncruces/go-strftime v0.1.9 // indirect 76 66 github.com/perimeterx/marshmallow v1.1.5 // indirect 77 67 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 78 - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 79 68 github.com/rivo/uniseg v0.4.7 // indirect 80 69 github.com/russross/blackfriday/v2 v2.1.0 // indirect 81 - github.com/sethvargo/go-retry v0.3.0 // indirect 82 70 github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect 83 71 github.com/spf13/pflag v1.0.5 // indirect 84 72 github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect 85 73 github.com/yuin/goldmark v1.7.4 // indirect 86 74 github.com/yuin/goldmark-emoji v1.0.3 // indirect 87 - go.uber.org/multierr v1.11.0 // indirect 88 75 golang.org/x/mod v0.21.0 // indirect 89 76 golang.org/x/sync v0.8.0 // indirect 90 77 golang.org/x/sys v0.25.0 // indirect ··· 92 79 golang.org/x/tools v0.25.0 // indirect 93 80 gopkg.in/yaml.v2 v2.4.0 // indirect 94 81 gopkg.in/yaml.v3 v3.0.1 // indirect 95 - modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect 96 - modernc.org/mathutil v1.6.0 // indirect 97 - modernc.org/memory v1.8.0 // indirect 98 - modernc.org/strutil v1.2.0 // indirect 99 - modernc.org/token v1.1.0 // indirect 100 82 )
-34
go.sum
··· 46 46 github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= 47 47 github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= 48 48 github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= 49 - github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 50 - github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 51 49 github.com/evanw/esbuild v0.24.0 h1:GZ78naTLp7FKr+K7eNuM/SLs5maeiHYRPsTg6kmdsSE= 52 50 github.com/evanw/esbuild v0.24.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 53 51 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= ··· 83 81 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 84 82 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 85 83 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 86 - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= 87 - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= 88 84 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 89 85 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 90 86 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= ··· 93 89 github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 94 90 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 95 91 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 96 - github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 97 - github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 98 92 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 99 93 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 100 94 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= ··· 136 130 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 137 131 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 138 132 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 139 - github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= 140 - github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= 141 133 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 142 134 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 143 135 github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= ··· 152 144 github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 153 145 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= 154 146 github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= 155 - github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 156 - github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 157 147 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 158 148 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 159 149 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= ··· 179 169 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 180 170 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 181 171 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 182 - github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= 183 - github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= 184 - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 185 - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 186 172 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 187 173 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 188 174 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 189 175 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 190 - github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 191 - github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 192 176 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 193 177 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 194 178 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 195 179 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 196 180 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 197 181 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 198 - github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= 199 - github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= 200 182 github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg= 201 183 github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc= 202 184 github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= ··· 223 205 github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 224 206 github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= 225 207 github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 226 - go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 227 - go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 228 208 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 229 209 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 230 210 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= ··· 312 292 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 313 293 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 314 294 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 315 - modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 316 - modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 317 - modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= 318 - modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= 319 - modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= 320 - modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= 321 - modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= 322 - modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= 323 - modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= 324 - modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= 325 - modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= 326 - modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= 327 - modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 328 - modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+22
utils/dirs.go
··· 1 + package utils 2 + 3 + import ( 4 + "os" 5 + "path/filepath" 6 + ) 7 + 8 + func RootDir() string { 9 + if env, ok := os.LookupEnv("SMALLWEB_DIR"); ok { 10 + return env 11 + } 12 + 13 + return filepath.Join(os.Getenv("HOME"), "smallweb") 14 + } 15 + 16 + func ConfigPath() string { 17 + return filepath.Join(RootDir(), ".smallweb", "config.json") 18 + } 19 + 20 + func DataDir() string { 21 + return filepath.Join(RootDir(), "data") 22 + }