forked from tangled.org/core
this repo has no description
at master 5.3 kB view raw
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 utf8.RuneCountInString(s.Filename) > 140 { 54 err = errors.Join(err, fmt.Errorf("filename too long")) 55 } 56 57 if utf8.RuneCountInString(s.Description) > 280 { 58 err = errors.Join(err, fmt.Errorf("description too long")) 59 } 60 61 if len(s.Contents) == 0 { 62 err = errors.Join(err, fmt.Errorf("contents is empty")) 63 } 64 65 return err 66} 67 68func (s *String) AsRecord() tangled.String { 69 return tangled.String{ 70 Filename: s.Filename, 71 Description: s.Description, 72 Contents: s.Contents, 73 CreatedAt: s.Created.Format(time.RFC3339), 74 } 75} 76 77func StringFromRecord(did, rkey string, record tangled.String) String { 78 created, err := time.Parse(record.CreatedAt, time.RFC3339) 79 if err != nil { 80 created = time.Now() 81 } 82 return String{ 83 Did: syntax.DID(did), 84 Rkey: rkey, 85 Filename: record.Filename, 86 Description: record.Description, 87 Contents: record.Contents, 88 Created: created, 89 } 90} 91 92func AddString(e Execer, s String) error { 93 _, err := e.Exec( 94 `insert into strings ( 95 did, 96 rkey, 97 filename, 98 description, 99 content, 100 created, 101 edited 102 ) 103 values (?, ?, ?, ?, ?, ?, null) 104 on conflict(did, rkey) do update set 105 filename = excluded.filename, 106 description = excluded.description, 107 content = excluded.content, 108 edited = case 109 when 110 strings.content != excluded.content 111 or strings.filename != excluded.filename 112 or strings.description != excluded.description then ? 113 else strings.edited 114 end`, 115 s.Did, 116 s.Rkey, 117 s.Filename, 118 s.Description, 119 s.Contents, 120 s.Created.Format(time.RFC3339), 121 time.Now().Format(time.RFC3339), 122 ) 123 return err 124} 125 126func GetStrings(e Execer, limit int, filters ...filter) ([]String, error) { 127 var all []String 128 129 var conditions []string 130 var args []any 131 for _, filter := range filters { 132 conditions = append(conditions, filter.Condition()) 133 args = append(args, filter.Arg()...) 134 } 135 136 whereClause := "" 137 if conditions != nil { 138 whereClause = " where " + strings.Join(conditions, " and ") 139 } 140 141 limitClause := "" 142 if limit != 0 { 143 limitClause = fmt.Sprintf(" limit %d ", limit) 144 } 145 146 query := fmt.Sprintf(`select 147 did, 148 rkey, 149 filename, 150 description, 151 content, 152 created, 153 edited 154 from strings 155 %s 156 order by created desc 157 %s`, 158 whereClause, 159 limitClause, 160 ) 161 162 rows, err := e.Query(query, args...) 163 164 if err != nil { 165 return nil, err 166 } 167 defer rows.Close() 168 169 for rows.Next() { 170 var s String 171 var createdAt string 172 var editedAt sql.NullString 173 174 if err := rows.Scan( 175 &s.Did, 176 &s.Rkey, 177 &s.Filename, 178 &s.Description, 179 &s.Contents, 180 &createdAt, 181 &editedAt, 182 ); err != nil { 183 return nil, err 184 } 185 186 s.Created, err = time.Parse(time.RFC3339, createdAt) 187 if err != nil { 188 s.Created = time.Now() 189 } 190 191 if editedAt.Valid { 192 e, err := time.Parse(time.RFC3339, editedAt.String) 193 if err != nil { 194 e = time.Now() 195 } 196 s.Edited = &e 197 } 198 199 all = append(all, s) 200 } 201 202 if err := rows.Err(); err != nil { 203 return nil, err 204 } 205 206 return all, nil 207} 208 209func CountStrings(e Execer, filters ...filter) (int64, error) { 210 var conditions []string 211 var args []any 212 for _, filter := range filters { 213 conditions = append(conditions, filter.Condition()) 214 args = append(args, filter.Arg()...) 215 } 216 217 whereClause := "" 218 if conditions != nil { 219 whereClause = " where " + strings.Join(conditions, " and ") 220 } 221 222 repoQuery := fmt.Sprintf(`select count(1) from strings %s`, whereClause) 223 var count int64 224 err := e.QueryRow(repoQuery, args...).Scan(&count) 225 226 if !errors.Is(err, sql.ErrNoRows) && err != nil { 227 return 0, err 228 } 229 230 return count, nil 231} 232 233func DeleteString(e Execer, filters ...filter) error { 234 var conditions []string 235 var args []any 236 for _, filter := range filters { 237 conditions = append(conditions, filter.Condition()) 238 args = append(args, filter.Arg()...) 239 } 240 241 whereClause := "" 242 if conditions != nil { 243 whereClause = " where " + strings.Join(conditions, " and ") 244 } 245 246 query := fmt.Sprintf(`delete from strings %s`, whereClause) 247 248 _, err := e.Exec(query, args...) 249 return err 250} 251 252func countLines(r io.Reader) (int, error) { 253 buf := make([]byte, 32*1024) 254 bufLen := 0 255 count := 0 256 nl := []byte{'\n'} 257 258 for { 259 c, err := r.Read(buf) 260 if c > 0 { 261 bufLen += c 262 } 263 count += bytes.Count(buf[:c], nl) 264 265 switch { 266 case err == io.EOF: 267 /* handle last line not having a newline at the end */ 268 if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' { 269 count++ 270 } 271 return count, nil 272 case err != nil: 273 return 0, err 274 } 275 } 276}