Fixed the Discord SSO somewhat
Fixed FS system Added TZ options
This commit is contained in:
@@ -1,123 +1,108 @@
|
||||
package index
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type opType string
|
||||
|
||||
const (
|
||||
OpPut opType = "put"
|
||||
OpDel opType = "del"
|
||||
)
|
||||
|
||||
type record struct {
|
||||
Op opType `json:"op"`
|
||||
Hash string `json:"hash"`
|
||||
Bytes int64 `json:"bytes,omitempty"`
|
||||
StoredAt time.Time `json:"stored_at,omitempty"`
|
||||
Private bool `json:"private,omitempty"`
|
||||
}
|
||||
|
||||
// Entry is the API/JSON shape the server returns.
|
||||
// StoredAt is RFC3339/RFC3339Nano in UTC.
|
||||
type Entry struct {
|
||||
Hash string `json:"hash"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
StoredAt time.Time `json:"stored_at"`
|
||||
Private bool `json:"private"`
|
||||
Hash string `json:"hash"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
StoredAt string `json:"stored_at"` // RFC3339( Nano ) string
|
||||
Private bool `json:"private"`
|
||||
CreatorTZ string `json:"creator_tz,omitempty"` // IANA TZ like "America/New_York"
|
||||
}
|
||||
|
||||
// internal record with real time.Time for sorting/comparison.
|
||||
type rec struct {
|
||||
Hash string
|
||||
Bytes int64
|
||||
StoredAt time.Time
|
||||
Private bool
|
||||
CreatorTZ string
|
||||
}
|
||||
|
||||
// Index is an in-memory index keyed by hash.
|
||||
type Index struct {
|
||||
path string
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
hash map[string]rec
|
||||
}
|
||||
|
||||
func New(baseDir string) *Index {
|
||||
return &Index{path: filepath.Join(baseDir, "index.jsonl")}
|
||||
// New creates an empty Index.
|
||||
func New() *Index {
|
||||
return &Index{
|
||||
hash: make(map[string]rec),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Index) AppendPut(e Entry) error {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
return appendRec(i.path, record{
|
||||
Op: OpPut,
|
||||
Hash: e.Hash,
|
||||
Bytes: e.Bytes,
|
||||
StoredAt: e.StoredAt,
|
||||
Private: e.Private,
|
||||
// Put inserts or replaces an entry.
|
||||
// e.StoredAt may be RFC3339( Nano ); if empty/invalid we use time.Now().UTC().
|
||||
func (ix *Index) Put(e Entry) error {
|
||||
ix.mu.Lock()
|
||||
defer ix.mu.Unlock()
|
||||
|
||||
t := parseWhen(e.StoredAt)
|
||||
if t.IsZero() {
|
||||
t = time.Now().UTC()
|
||||
}
|
||||
|
||||
ix.hash[e.Hash] = rec{
|
||||
Hash: e.Hash,
|
||||
Bytes: e.Bytes,
|
||||
StoredAt: t,
|
||||
Private: e.Private,
|
||||
CreatorTZ: e.CreatorTZ,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes an entry by hash (no error if absent).
|
||||
func (ix *Index) Delete(hash string) error {
|
||||
ix.mu.Lock()
|
||||
defer ix.mu.Unlock()
|
||||
delete(ix.hash, hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// List returns entries sorted by StoredAt descending.
|
||||
func (ix *Index) List() ([]Entry, error) {
|
||||
ix.mu.RLock()
|
||||
defer ix.mu.RUnlock()
|
||||
|
||||
tmp := make([]rec, 0, len(ix.hash))
|
||||
for _, r := range ix.hash {
|
||||
tmp = append(tmp, r)
|
||||
}
|
||||
sort.Slice(tmp, func(i, j int) bool {
|
||||
return tmp[i].StoredAt.After(tmp[j].StoredAt)
|
||||
})
|
||||
}
|
||||
|
||||
func (i *Index) AppendDelete(hash string) error {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
return appendRec(i.path, record{Op: OpDel, Hash: hash})
|
||||
}
|
||||
|
||||
func appendRec(path string, r record) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
enc := json.NewEncoder(f)
|
||||
return enc.Encode(r)
|
||||
}
|
||||
|
||||
func (i *Index) Snapshot() ([]Entry, error) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
f, err := os.Open(i.path)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
|
||||
|
||||
type state struct {
|
||||
Entry Entry
|
||||
Deleted bool
|
||||
}
|
||||
m := make(map[string]state)
|
||||
for sc.Scan() {
|
||||
var rec record
|
||||
if err := json.Unmarshal(sc.Bytes(), &rec); err != nil {
|
||||
continue
|
||||
}
|
||||
switch rec.Op {
|
||||
case OpPut:
|
||||
m[rec.Hash] = state{Entry: Entry{
|
||||
Hash: rec.Hash, Bytes: rec.Bytes, StoredAt: rec.StoredAt, Private: rec.Private,
|
||||
}}
|
||||
case OpDel:
|
||||
s := m[rec.Hash]
|
||||
s.Deleted = true
|
||||
m[rec.Hash] = s
|
||||
out := make([]Entry, len(tmp))
|
||||
for i, r := range tmp {
|
||||
out[i] = Entry{
|
||||
Hash: r.Hash,
|
||||
Bytes: r.Bytes,
|
||||
StoredAt: r.StoredAt.UTC().Format(time.RFC3339Nano),
|
||||
Private: r.Private,
|
||||
CreatorTZ: r.CreatorTZ,
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Entry
|
||||
for _, s := range m {
|
||||
if !s.Deleted && s.Entry.Hash != "" {
|
||||
out = append(out, s.Entry)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].StoredAt.After(out[j].StoredAt) })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// parseWhen tries RFC3339Nano then RFC3339; returns zero time on failure.
|
||||
func parseWhen(s string) time.Time {
|
||||
if s == "" {
|
||||
return time.Time{}
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
|
||||
return t
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339, s); err == nil {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
Reference in New Issue
Block a user