Fix install script: strip v prefix from version for archive name

This commit is contained in:
Harivansh Rathi 2026-04-01 21:06:40 -04:00
parent 8002158a45
commit 1d564b738d
16 changed files with 552 additions and 26 deletions

View file

@ -1,6 +1,8 @@
package main
import (
"sort"
"strings"
"time"
)
@ -10,6 +12,7 @@ type appConfig struct {
statePath string
dbPath string
sessionTTL time.Duration
nodeOfflineThreshold time.Duration
registrationEnabled bool
corsOrigin string
}
@ -21,7 +24,13 @@ type app struct {
store store
}
const defaultNodeOfflineThreshold = 2 * time.Minute
func newApp(config appConfig, startedAt time.Time) (*app, error) {
if config.nodeOfflineThreshold <= 0 {
config.nodeOfflineThreshold = defaultNodeOfflineThreshold
}
var s store
var err error
if config.dbPath != "" {
@ -41,6 +50,68 @@ func newApp(config appConfig, startedAt time.Time) (*app, error) {
}, nil
}
func (a *app) presentedNode(node nasNode) nasNode {
presented := copyNasNode(node)
if !nodeHeartbeatIsFresh(presented.LastSeenAt, a.now().UTC(), a.config.nodeOfflineThreshold) {
presented.Status = "offline"
}
return presented
}
func (a *app) listNodes(ownerID string) []nasNode {
nodes := a.store.listNodes(ownerID)
presented := make([]nasNode, 0, len(nodes))
for _, node := range nodes {
presented = append(presented, a.presentedNode(node))
}
sort.Slice(presented, func(i, j int) bool {
return presented[i].ID < presented[j].ID
})
return presented
}
func (a *app) listConnectedExports(ownerID string) []storageExport {
exports := a.store.listExports(ownerID)
connected := make([]storageExport, 0, len(exports))
for _, export := range exports {
context, ok := a.store.exportContext(export.ID, ownerID)
if !ok {
continue
}
if !nodeIsConnected(a.presentedNode(context.node)) {
continue
}
connected = append(connected, export)
}
return connected
}
func nodeHeartbeatIsFresh(lastSeenAt string, referenceTime time.Time, threshold time.Duration) bool {
lastSeenAt = strings.TrimSpace(lastSeenAt)
if threshold <= 0 || lastSeenAt == "" {
return false
}
parsedLastSeenAt, err := time.Parse(time.RFC3339, lastSeenAt)
if err != nil {
return false
}
referenceTime = referenceTime.UTC()
if parsedLastSeenAt.After(referenceTime) {
return true
}
return referenceTime.Sub(parsedLastSeenAt) <= threshold
}
func nodeIsConnected(node nasNode) bool {
return node.Status == "online" || node.Status == "degraded"
}
type nextcloudBackendStatus struct {
Configured bool `json:"configured"`
BaseURL string `json:"baseUrl"`

View file

@ -36,6 +36,16 @@ func newAppFromEnv(startedAt time.Time) (*app, error) {
sessionTTL = parsedSessionTTL
}
nodeOfflineThreshold := defaultNodeOfflineThreshold
rawNodeOfflineThreshold := strings.TrimSpace(env("BETTERNAS_NODE_OFFLINE_THRESHOLD", "2m"))
if rawNodeOfflineThreshold != "" {
parsedNodeOfflineThreshold, err := time.ParseDuration(rawNodeOfflineThreshold)
if err != nil {
return nil, err
}
nodeOfflineThreshold = parsedNodeOfflineThreshold
}
app, err := newApp(
appConfig{
version: env("BETTERNAS_VERSION", "0.1.0-dev"),
@ -43,6 +53,7 @@ func newAppFromEnv(startedAt time.Time) (*app, error) {
statePath: env("BETTERNAS_CONTROL_PLANE_STATE_PATH", ".state/control-plane/state.json"),
dbPath: env("BETTERNAS_CONTROL_PLANE_DB_PATH", ".state/control-plane/betternas.db"),
sessionTTL: sessionTTL,
nodeOfflineThreshold: nodeOfflineThreshold,
registrationEnabled: env("BETTERNAS_REGISTRATION_ENABLED", "true") == "true",
corsOrigin: env("BETTERNAS_CORS_ORIGIN", ""),
},

View file

@ -36,6 +36,7 @@ func (a *app) handler() http.Handler {
mux.HandleFunc("POST /api/v1/auth/login", a.handleAuthLogin)
mux.HandleFunc("POST /api/v1/auth/logout", a.handleAuthLogout)
mux.HandleFunc("GET /api/v1/auth/me", a.handleAuthMe)
mux.HandleFunc("GET /api/v1/nodes", a.handleNodesList)
mux.HandleFunc("POST /api/v1/nodes/register", a.handleNodeRegister)
mux.HandleFunc("POST /api/v1/nodes/{nodeId}/heartbeat", a.handleNodeHeartbeat)
mux.HandleFunc("PUT /api/v1/nodes/{nodeId}/exports", a.handleNodeExports)
@ -74,6 +75,15 @@ func (a *app) handleVersion(w http.ResponseWriter, _ *http.Request) {
})
}
func (a *app) handleNodesList(w http.ResponseWriter, r *http.Request) {
currentUser, ok := a.requireSessionUser(w, r)
if !ok {
return
}
writeJSON(w, http.StatusOK, a.listNodes(currentUser.ID))
}
func (a *app) handleNodeRegister(w http.ResponseWriter, r *http.Request) {
currentUser, ok := a.requireSessionUser(w, r)
if !ok {
@ -177,7 +187,7 @@ func (a *app) handleExportsList(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(w, http.StatusOK, a.store.listExports(currentUser.ID))
writeJSON(w, http.StatusOK, a.listConnectedExports(currentUser.ID))
}
func (a *app) handleMountProfileIssue(w http.ResponseWriter, r *http.Request) {
@ -202,6 +212,11 @@ func (a *app) handleMountProfileIssue(w http.ResponseWriter, r *http.Request) {
http.Error(w, errExportNotFound.Error(), http.StatusNotFound)
return
}
context.node = a.presentedNode(context.node)
if !nodeIsConnected(context.node) {
http.Error(w, errMountTargetUnavailable.Error(), http.StatusServiceUnavailable)
return
}
mountURL, err := buildMountURL(context)
if err != nil {

View file

@ -346,6 +346,27 @@ func (s *sqliteStore) listExports(ownerID string) []storageExport {
return exports
}
func (s *sqliteStore) listNodes(ownerID string) []nasNode {
rows, err := s.db.Query("SELECT id, machine_id, owner_id, display_name, agent_version, status, last_seen_at, direct_address, relay_address FROM nodes WHERE owner_id = ? ORDER BY id", ownerID)
if err != nil {
return nil
}
defer rows.Close()
var nodes []nasNode
for rows.Next() {
node := s.scanNode(rows)
if node.ID != "" {
nodes = append(nodes, node)
}
}
if nodes == nil {
nodes = []nasNode{}
}
return nodes
}
func (s *sqliteStore) listExportsForNode(nodeID string) []storageExport {
rows, err := s.db.Query("SELECT id, node_id, owner_id, label, path, mount_path, capacity_bytes FROM exports WHERE node_id = ? ORDER BY id", nodeID)
if err != nil {
@ -401,15 +422,29 @@ func (s *sqliteStore) exportContext(exportID string, ownerID string) (exportCont
}
func (s *sqliteStore) nodeByID(nodeID string) (nasNode, bool) {
row := s.db.QueryRow(
"SELECT id, machine_id, owner_id, display_name, agent_version, status, last_seen_at, direct_address, relay_address FROM nodes WHERE id = ?",
nodeID)
n := s.scanNode(row)
if n.ID == "" {
return nasNode{}, false
}
return n, true
}
type sqliteNodeScanner interface {
Scan(dest ...any) error
}
func (s *sqliteStore) scanNode(scanner sqliteNodeScanner) nasNode {
var n nasNode
var directAddr, relayAddr sql.NullString
var lastSeenAt sql.NullString
var ownerID sql.NullString
err := s.db.QueryRow(
"SELECT id, machine_id, owner_id, display_name, agent_version, status, last_seen_at, direct_address, relay_address FROM nodes WHERE id = ?",
nodeID).Scan(&n.ID, &n.MachineID, &ownerID, &n.DisplayName, &n.AgentVersion, &n.Status, &lastSeenAt, &directAddr, &relayAddr)
err := scanner.Scan(&n.ID, &n.MachineID, &ownerID, &n.DisplayName, &n.AgentVersion, &n.Status, &lastSeenAt, &directAddr, &relayAddr)
if err != nil {
return nasNode{}, false
return nasNode{}
}
if ownerID.Valid {
n.OwnerID = ownerID.String
@ -423,7 +458,7 @@ func (s *sqliteStore) nodeByID(nodeID string) (nasNode, bool) {
if relayAddr.Valid {
n.RelayAddress = &relayAddr.String
}
return n, true
return n
}
func (s *sqliteStore) nodeAuthByMachineID(machineID string) (nodeAuthState, bool) {

View file

@ -320,6 +320,25 @@ func (s *memoryStore) listExports(ownerID string) []storageExport {
return exports
}
func (s *memoryStore) listNodes(ownerID string) []nasNode {
s.mu.RLock()
defer s.mu.RUnlock()
nodes := make([]nasNode, 0, len(s.state.NodesByID))
for _, node := range s.state.NodesByID {
if node.OwnerID != ownerID {
continue
}
nodes = append(nodes, copyNasNode(node))
}
sort.Slice(nodes, func(i, j int) bool {
return nodes[i].ID < nodes[j].ID
})
return nodes
}
func (s *memoryStore) exportContext(exportID string, ownerID string) (exportContext, bool) {
s.mu.RLock()
defer s.mu.RUnlock()

View file

@ -9,6 +9,7 @@ type store interface {
upsertExports(nodeID string, ownerID string, request nodeExportsRequest) ([]storageExport, error)
recordHeartbeat(nodeID string, ownerID string, request nodeHeartbeatRequest) error
listExports(ownerID string) []storageExport
listNodes(ownerID string) []nasNode
exportContext(exportID string, ownerID string) (exportContext, bool)
nodeByID(nodeID string) (nasNode, bool)