forked from tangled.org/core
this repo has no description
1package db 2 3import ( 4 "bytes" 5 "database/sql" 6 "errors" 7 "fmt" 8 "io" 9 "strings" 10 "time" 11 "unicode/utf8" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "tangled.sh/tangled.sh/core/api/tangled" 15) 16 17type String struct { 18 Did syntax.DID 19 Rkey string 20 21 Filename string 22 Description string 23 Contents string 24 Created time.Time 25 Edited *time.Time 26} 27 28func (s *String) StringAt() syntax.ATURI { 29 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", s.Did, tangled.StringNSID, s.Rkey)) 30} 31 32type StringStats struct { 33 LineCount uint64 34 ByteCount uint64 35} 36 37func (s String) Stats() StringStats { 38 lineCount, err := countLines(strings.NewReader(s.Contents)) 39 if err != nil { 40 // non-fatal 41 // TODO: log this? 42 } 43 44 return StringStats{ 45 LineCount: uint64(lineCount), 46 ByteCount: uint64(len(s.Contents)), 47 } 48} 49 50func (s String) Validate() error { 51 var err error 52 53 if !strings.Contains(s.Filename, ".") { 54 err = errors.Join(err, fmt.Errorf("missing filename extension")) 55 } 56 57 if strings.HasSuffix(s.Filename, ".") { 58 err = errors.Join(err, fmt.Errorf("filename ends with `.`")) 59 } 60 61 if utf8.RuneCountInString(s.Filename) > 140 { 62 err = errors.Join(err, fmt.Errorf("filename too long")) 63 } 64 65 if utf8.RuneCountInString(s.Description) > 280 { 66 err = errors.Join(err, fmt.Errorf("description too long")) 67 } 68 69 if len(s.Contents) == 0 { 70 err = errors.Join(err, fmt.Errorf("contents is empty")) 71 } 72 73 return err 74} 75 76func (s *String) AsRecord() tangled.String { 77 return tangled.String{ 78 Filename: s.Filename, 79 Description: s.Description, 80 Contents: s.Contents, 81 CreatedAt: s.Created.Format(time.RFC3339), 82 } 83} 84 85func StringFromRecord(did, rkey string, record tangled.String) String { 86 created, err := time.Parse(record.CreatedAt, time.RFC3339) 87 if err != nil { 88 created = time.Now() 89 } 90 return String{ 91 Did: syntax.DID(did), 92 Rkey: rkey, 93 Filename: record.Filename, 94 Description: record.Description, 95 Contents: record.Contents, 96 Created: created, 97 } 98} 99 100func AddString(e Execer, s String) error { 101 _, err := e.Exec( 102 `insert into strings ( 103 did, 104 rkey, 105 filename, 106 description, 107 content, 108 created, 109 edited 110 ) 111 values (?, ?, ?, ?, ?, ?, null) 112 on conflict(did, rkey) do update set 113 filename = excluded.filename, 114 description = excluded.description, 115 content = excluded.content, 116 edited = case 117 when 118 strings.content != excluded.content 119 or strings.filename != excluded.filename 120 or strings.description != excluded.description then ? 121 else strings.edited 122 end`, 123 s.Did, 124 s.Rkey, 125 s.Filename, 126 s.Description, 127 s.Contents, 128 s.Created.Format(time.RFC3339), 129 time.Now().Format(time.RFC3339), 130 ) 131 return err 132} 133 134func GetStrings(e Execer, limit int, filters ...filter) ([]String, error) { 135 var all []String 136 137 var conditions []string 138 var args []any 139 for _, filter := range filters { 140 conditions = append(conditions, filter.Condition()) 141 args = append(args, filter.Arg()...) 142 } 143 144 whereClause := "" 145 if conditions != nil { 146 whereClause = " where " + strings.Join(conditions, " and ") 147 } 148 149 limitClause := "" 150 if limit != 0 { 151 limitClause = fmt.Sprintf(" limit %d ", limit) 152 } 153 154 query := fmt.Sprintf(`select 155 did, 156 rkey, 157 filename, 158 description, 159 content, 160 created, 161 edited 162 from strings 163 %s 164 order by created desc 165 %s`, 166 whereClause, 167 limitClause, 168 ) 169 170 rows, err := e.Query(query, args...) 171 172 if err != nil { 173 return nil, err 174 } 175 defer rows.Close() 176 177 for rows.Next() { 178 var s String 179 var createdAt string 180 var editedAt sql.NullString 181 182 if err := rows.Scan( 183 &s.Did, 184 &s.Rkey, 185 &s.Filename, 186 &s.Description, 187 &s.Contents, 188 &createdAt, 189 &editedAt, 190 ); err != nil { 191 return nil, err 192 } 193 194 s.Created, err = time.Parse(time.RFC3339, createdAt) 195 if err != nil { 196 s.Created = time.Now() 197 } 198 199 if editedAt.Valid { 200 e, err := time.Parse(time.RFC3339, editedAt.String) 201 if err != nil { 202 e = time.Now() 203 } 204 s.Edited = &e 205 } 206 207 all = append(all, s) 208 } 209 210 if err := rows.Err(); err != nil { 211 return nil, err 212 } 213 214 return all, nil 215} 216 217func DeleteString(e Execer, filters ...filter) error { 218 var conditions []string 219 var args []any 220 for _, filter := range filters { 221 conditions = append(conditions, filter.Condition()) 222 args = append(args, filter.Arg()...) 223 } 224 225 whereClause := "" 226 if conditions != nil { 227 whereClause = " where " + strings.Join(conditions, " and ") 228 } 229 230 query := fmt.Sprintf(`delete from strings %s`, whereClause) 231 232 _, err := e.Exec(query, args...) 233 return err 234} 235 236func countLines(r io.Reader) (int, error) { 237 buf := make([]byte, 32*1024) 238 bufLen := 0 239 count := 0 240 nl := []byte{'\n'} 241 242 for { 243 c, err := r.Read(buf) 244 if c > 0 { 245 bufLen += c 246 } 247 count += bytes.Count(buf[:c], nl) 248 249 switch { 250 case err == io.EOF: 251 /* handle last line not having a newline at the end */ 252 if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' { 253 count++ 254 } 255 return count, nil 256 case err != nil: 257 return 0, err 258 } 259 } 260}