storage.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. package storage
  2. import (
  3. "bytes"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log"
  9. "mime/multipart"
  10. "os"
  11. "path"
  12. "strings"
  13. "time"
  14. "git.aetherial.dev/aeth/keiji/pkg/env"
  15. "github.com/google/uuid"
  16. )
  17. const TECHNICAL = "technical"
  18. const CONFIGURATION = "configuration"
  19. const BLOG = "blog"
  20. const CREATIVE = "creative"
  21. const DIGITAL_ART = "digital_art"
  22. const MISC_ASSET = "misc_asset"
  23. const HOMEPAGE = "homepage"
  24. var Topics = []string{
  25. TECHNICAL,
  26. BLOG,
  27. CREATIVE,
  28. HOMEPAGE,
  29. }
  30. var ImageCategories = []string{
  31. MISC_ASSET,
  32. DIGITAL_ART,
  33. }
  34. type DatabaseSchema struct {
  35. // Gotta figure out what this looks like
  36. // so that the ExtractAll() function gets
  37. // all of the data from the database
  38. }
  39. type MenuElement struct {
  40. Png string `json:"png"`
  41. Category string `json:"category"`
  42. MenuLinks []LinkPair `json:"menu_links"`
  43. }
  44. type AdminPage struct {
  45. Tables map[string][]TableData `json:"tables"`
  46. }
  47. type TableData struct { // TODO: add this to the database io interface
  48. DisplayName string `json:"display_name"`
  49. Link string `json:"link"`
  50. }
  51. type LinkPair struct {
  52. Link string `json:"link"`
  53. Text string `json:"text"`
  54. }
  55. type NavBarItem struct {
  56. Png []byte `json:"png"`
  57. File *multipart.FileHeader `form:"file"`
  58. Link string `json:"link" form:"link"`
  59. Redirect string `json:"redirect" form:"redirect"`
  60. }
  61. type Asset struct {
  62. Name string
  63. Data []byte
  64. }
  65. type Identifier string
  66. type Document struct {
  67. Row int
  68. Ident Identifier `json:"id"`
  69. Title string `json:"title"`
  70. Created string `json:"created"`
  71. Body string `json:"body"`
  72. Category string `json:"category"`
  73. Sample string `json:"sample"`
  74. }
  75. /*
  76. Truncates a text post into a 256 character long 'sample' for displaying posts
  77. */
  78. func (d *Document) MakeSample() string {
  79. t := strings.Split(d.Body, "")
  80. var sample []string
  81. if len(d.Body) < 256 {
  82. return d.Body
  83. }
  84. for i := 0; i < 256; i++ {
  85. sample = append(sample, t[i])
  86. }
  87. sample = append(sample, " ...")
  88. return strings.Join(sample, "")
  89. }
  90. type Image struct {
  91. Ident Identifier `json:"identifier"`
  92. Location string
  93. Title string `json:"title" form:"title"`
  94. File *multipart.FileHeader `form:"file"`
  95. Desc string `json:"description" form:"description"`
  96. Created string
  97. Category string `json:"category" form:"category"`
  98. MediaType string `json:"media_type"`
  99. Data []byte
  100. }
  101. type DocumentIO interface {
  102. GetDocument(id Identifier) (Document, error)
  103. GetImage(id Identifier) (Image, error)
  104. GetAllImages() []Image
  105. GetImagesByCategory(category string) []Image
  106. UpdateDocument(doc Document) error
  107. DeleteDocument(id Identifier) error
  108. UpdateDigitalArt(id Identifier, title, desc string) error
  109. ArchiveDigitalArt(id Identifier) error
  110. DeleteNavbarItem(id Identifier) error
  111. AddDocument(doc Document) (Identifier, error)
  112. AddImage(data []byte, title, desc, category, mediaType string) (Identifier, error)
  113. AddAsset(name string, data []byte) error
  114. AddAdminTableEntry(TableData, string) error
  115. AddNavbarItem(NavBarItem) error
  116. AddMenuItem(LinkPair) error
  117. GetByCategory(category string) []Document
  118. AllDocuments() []Document
  119. GetDropdownElements() []LinkPair
  120. GetNavBarLinks() []NavBarItem
  121. GetAssets() []Asset
  122. GetAdminTables() AdminPage
  123. }
  124. // DocumentListReader provides list operations that distinguish database failures
  125. // from empty results. Request handlers should prefer these methods.
  126. type DocumentListReader interface {
  127. GetDropdownElementsWithError() ([]LinkPair, error)
  128. GetNavBarLinksWithError() ([]NavBarItem, error)
  129. GetAssetsWithError() ([]Asset, error)
  130. GetAdminTablesWithError() (AdminPage, error)
  131. GetByCategoryWithError(string) ([]Document, error)
  132. AllDocumentsWithError() ([]Document, error)
  133. GetAllImagesWithError() ([]Image, error)
  134. GetImagesByCategoryWithError(string) ([]Image, error)
  135. }
  136. var (
  137. ErrDuplicate = errors.New("record already exists")
  138. ErrNotExists = errors.New("row not exists")
  139. ErrUpdateFailed = errors.New("update failed")
  140. ErrDeleteFailed = errors.New("delete failed")
  141. )
  142. type SQLiteRepo struct {
  143. db *sql.DB
  144. imageIO ImageIO
  145. }
  146. type ImageIO interface {
  147. Put([]byte, Identifier) error
  148. Get(Identifier) ([]byte, error)
  149. }
  150. // ReadSeekCloser is a seekable media stream that must be closed by its caller.
  151. type ReadSeekCloser interface {
  152. io.Reader
  153. io.Seeker
  154. io.Closer
  155. }
  156. // ImageOpener is an optional streaming extension to ImageIO.
  157. type ImageOpener interface {
  158. Open(Identifier) (ReadSeekCloser, error)
  159. }
  160. type FilesystemImageIO struct {
  161. RootDir string
  162. }
  163. /*
  164. create a new FilesystemImageIO struct
  165. */
  166. func MustNewFilesystemImageIO(path string) FilesystemImageIO {
  167. var filemode os.FileMode
  168. filemode = 0775
  169. err := os.MkdirAll(path, filemode)
  170. if err != nil {
  171. log.Fatalf("Error creating path: '%s' with permissions: '%d'. Error: %s\n", path, filemode, err.Error())
  172. }
  173. return FilesystemImageIO{RootDir: path}
  174. }
  175. /*
  176. Put a data blob on the filesystem
  177. :param b: the
  178. */
  179. func (f FilesystemImageIO) Put(b []byte, id Identifier) error {
  180. if err := validateImageIdentifier(id); err != nil {
  181. return err
  182. }
  183. fh, err := os.OpenFile(path.Join(f.RootDir, string(id)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
  184. if err != nil {
  185. return err
  186. }
  187. defer fh.Close()
  188. _, err = fh.Write(b)
  189. if err != nil {
  190. return err
  191. }
  192. return nil
  193. }
  194. /*
  195. Get a data blob from the filesystem
  196. :param id: the identifier of the image to retrieve
  197. */
  198. func (f FilesystemImageIO) Get(id Identifier) ([]byte, error) {
  199. fh, err := f.Open(id)
  200. if err != nil {
  201. return nil, err
  202. }
  203. defer fh.Close()
  204. b, err := io.ReadAll(fh)
  205. if err != nil {
  206. return nil, err
  207. }
  208. return b, nil
  209. }
  210. // Open opens a filesystem image without reading it into memory.
  211. func (f FilesystemImageIO) Open(id Identifier) (ReadSeekCloser, error) {
  212. if err := validateImageIdentifier(id); err != nil {
  213. return nil, err
  214. }
  215. return os.Open(path.Join(f.RootDir, string(id)))
  216. }
  217. func validateImageIdentifier(id Identifier) error {
  218. name := string(id)
  219. if name == "" || name == "." || name == ".." || strings.ContainsAny(name, "/\\\x00") {
  220. return fmt.Errorf("invalid image identifier %q", name)
  221. }
  222. return nil
  223. }
  224. // Instantiate a new SQLiteRepo struct
  225. func NewSQLiteRepo(db *sql.DB, imgIo ImageIO) *SQLiteRepo {
  226. return &SQLiteRepo{
  227. db: db,
  228. imageIO: imgIo,
  229. }
  230. }
  231. // Creates a new SQL table for text posts
  232. func (r *SQLiteRepo) Migrate(seedQueries []string) error {
  233. for i := range seedQueries {
  234. _, err := r.db.Exec(seedQueries[i])
  235. if err != nil {
  236. return err
  237. }
  238. }
  239. // CREATE TABLE IF NOT EXISTS does not update an existing images table.
  240. // Check by name so migration remains safe to run repeatedly.
  241. rows, err := r.db.Query("PRAGMA table_info(images)")
  242. if err != nil {
  243. return err
  244. }
  245. var hasImages, hasMediaType bool
  246. for rows.Next() {
  247. var cid, notNull, primaryKey int
  248. var name, columnType string
  249. var defaultValue sql.NullString
  250. if err := rows.Scan(&cid, &name, &columnType, &notNull, &defaultValue, &primaryKey); err != nil {
  251. rows.Close()
  252. return err
  253. }
  254. hasImages = true
  255. if name == "media_type" {
  256. hasMediaType = true
  257. }
  258. }
  259. err = rows.Err()
  260. rows.Close()
  261. if err != nil {
  262. return err
  263. }
  264. if hasImages && !hasMediaType {
  265. _, err = r.db.Exec("ALTER TABLE images ADD COLUMN media_type TEXT NOT NULL DEFAULT ''")
  266. return err
  267. }
  268. return nil
  269. }
  270. /*
  271. Get all dropdown menu elements. Returns a list of LinkPair structs with the text and redirect location
  272. */
  273. func (s *SQLiteRepo) GetDropdownElements() []LinkPair {
  274. items, err := s.GetDropdownElementsWithError()
  275. if err != nil {
  276. log.Printf("get dropdown elements: %v", err)
  277. }
  278. return items
  279. }
  280. func (s *SQLiteRepo) GetDropdownElementsWithError() ([]LinkPair, error) {
  281. rows, err := s.db.Query("SELECT * FROM menu")
  282. if err != nil {
  283. return nil, err
  284. }
  285. defer rows.Close()
  286. var items []LinkPair
  287. for rows.Next() {
  288. var id int
  289. var item LinkPair
  290. if err := rows.Scan(&id, &item.Link, &item.Text); err != nil {
  291. return nil, err
  292. }
  293. items = append(items, item)
  294. }
  295. if err := rows.Err(); err != nil {
  296. return nil, err
  297. }
  298. return items, nil
  299. }
  300. /*
  301. Retrieve a dropdown element by its text name on the UI
  302. */
  303. func (s *SQLiteRepo) GetMenuItemByName(link, text string) (LinkPair, bool) {
  304. fmt.Printf("From GetMenuItemByName: Link: %s Text: %s\n", link, text)
  305. rows := s.db.QueryRow("SELECT * FROM menu WHERE link = ? AND text = ?", link, text)
  306. var item LinkPair
  307. var id int
  308. err := rows.Scan(&id, &item.Link, &item.Text)
  309. if err != nil {
  310. if errors.Is(err, sql.ErrNoRows) {
  311. return item, false
  312. } else {
  313. return item, false
  314. }
  315. }
  316. return item, true
  317. }
  318. // get Admin table entry by its display name, link, and category.
  319. func (s *SQLiteRepo) GetAdminTableEntry(displayName, link, category string) (TableData, bool) {
  320. rows := s.db.QueryRow("SELECT * FROM admin WHERE display_name = ? AND link = ? AND category = ?", displayName, link, category)
  321. var item TableData
  322. var id int
  323. err := rows.Scan(&id, &item.DisplayName, &item.Link, &category)
  324. if err != nil {
  325. if errors.Is(err, sql.ErrNoRows) {
  326. return item, false
  327. }
  328. log.Printf("Error getting admin table entry: %s", err.Error())
  329. return item, false
  330. }
  331. return item, true
  332. }
  333. // get navbar entry.
  334. func (s *SQLiteRepo) GetNavbarLink(link, redirect string) (NavBarItem, bool) {
  335. rows := s.db.QueryRow("SELECT * FROM navbar WHERE link = ? AND redirect = ?", link, redirect)
  336. var item NavBarItem
  337. var id int
  338. err := rows.Scan(&id, &item.Png, &item.Link, &item.Redirect)
  339. if err != nil {
  340. if errors.Is(err, sql.ErrNoRows) {
  341. return item, false
  342. }
  343. log.Printf("Error scanning for item: %s", err.Error())
  344. return item, false
  345. }
  346. return item, true
  347. }
  348. // get an asset from the store
  349. func (s *SQLiteRepo) GetAsset(name string) (Asset, bool) {
  350. rows := s.db.QueryRow("SELECT * FROM assets WHERE name = ?", name)
  351. var item Asset
  352. var id int
  353. err := rows.Scan(&id, &item.Name, &item.Data)
  354. if err != nil {
  355. if errors.Is(err, sql.ErrNoRows) {
  356. return item, false
  357. }
  358. log.Printf("Error getting asset: %s", err.Error())
  359. return item, false
  360. }
  361. return item, true
  362. }
  363. /*
  364. Get all nav bar items. Returns a list of NavBarItem structs with the png data, the file name, and the redirect location of the icon
  365. /*
  366. Get all nav bar items. Returns a list of NavBarItem structs with the png data, the file name, and the redirect location of the icon
  367. */
  368. func (s *SQLiteRepo) GetNavBarLinks() []NavBarItem {
  369. items, err := s.GetNavBarLinksWithError()
  370. if err != nil {
  371. log.Printf("get navbar links: %v", err)
  372. }
  373. return items
  374. }
  375. func (s *SQLiteRepo) GetNavBarLinksWithError() ([]NavBarItem, error) {
  376. rows, err := s.db.Query("SELECT * FROM navbar")
  377. if err != nil {
  378. return nil, err
  379. }
  380. defer rows.Close()
  381. var items []NavBarItem
  382. for rows.Next() {
  383. var item NavBarItem
  384. var id int
  385. if err := rows.Scan(&id, &item.Png, &item.Link, &item.Redirect); err != nil {
  386. return nil, err
  387. }
  388. items = append(items, item)
  389. }
  390. if err := rows.Err(); err != nil {
  391. return nil, err
  392. }
  393. return items, nil
  394. }
  395. func (s *SQLiteRepo) GetAssets() []Asset {
  396. items, err := s.GetAssetsWithError()
  397. if err != nil {
  398. log.Printf("get assets: %v", err)
  399. }
  400. return items
  401. }
  402. func (s *SQLiteRepo) GetAssetsWithError() ([]Asset, error) {
  403. rows, err := s.db.Query("SELECT * FROM assets")
  404. if err != nil {
  405. return nil, err
  406. }
  407. defer rows.Close()
  408. var items []Asset
  409. for rows.Next() {
  410. var item Asset
  411. var id int
  412. if err := rows.Scan(&id, &item.Name, &item.Data); err != nil {
  413. return nil, err
  414. }
  415. items = append(items, item)
  416. }
  417. if err := rows.Err(); err != nil {
  418. return nil, err
  419. }
  420. return items, nil
  421. }
  422. func (s *SQLiteRepo) GetAdminTables() AdminPage {
  423. page, err := s.GetAdminTablesWithError()
  424. if err != nil {
  425. log.Printf("get admin tables: %v", err)
  426. }
  427. return page
  428. }
  429. func (s *SQLiteRepo) GetAdminTablesWithError() (AdminPage, error) {
  430. rows, err := s.db.Query("SELECT * FROM admin")
  431. if err != nil {
  432. return AdminPage{}, err
  433. }
  434. defer rows.Close()
  435. page := AdminPage{Tables: map[string][]TableData{}}
  436. for rows.Next() {
  437. var item TableData
  438. var id int
  439. var category string
  440. if err := rows.Scan(&id, &item.DisplayName, &item.Link, &category); err != nil {
  441. return AdminPage{}, err
  442. }
  443. page.Tables[category] = append(page.Tables[category], item)
  444. }
  445. if err := rows.Err(); err != nil {
  446. return AdminPage{}, err
  447. }
  448. return page, nil
  449. }
  450. /*
  451. Retrieve a document from the sqlite db
  452. :param id: the Identifier of the post
  453. */
  454. func (s *SQLiteRepo) GetDocument(id Identifier) (Document, error) {
  455. row := s.db.QueryRow("SELECT * FROM posts WHERE id = ?", id)
  456. var post Document
  457. var rowNum int
  458. if err := row.Scan(&rowNum, &post.Ident, &post.Title, &post.Created, &post.Body, &post.Category, &post.Sample); err != nil {
  459. if errors.Is(err, sql.ErrNoRows) {
  460. return post, ErrNotExists
  461. }
  462. return post, err
  463. }
  464. return post, nil
  465. }
  466. /*
  467. Get all documents by category
  468. :param category: the category to retrieve all docs from
  469. */
  470. func (s *SQLiteRepo) GetByCategory(category string) []Document {
  471. docs, err := s.GetByCategoryWithError(category)
  472. if err != nil {
  473. log.Printf("get documents by category: %v", err)
  474. }
  475. return docs
  476. }
  477. func (s *SQLiteRepo) GetByCategoryWithError(category string) ([]Document, error) {
  478. return s.listDocuments("SELECT * FROM posts WHERE category = ?", category)
  479. }
  480. func (s *SQLiteRepo) listDocuments(query string, args ...interface{}) ([]Document, error) {
  481. rows, err := s.db.Query(query, args...)
  482. if err != nil {
  483. return nil, err
  484. }
  485. defer rows.Close()
  486. var docs []Document
  487. for rows.Next() {
  488. var doc Document
  489. if err := rows.Scan(&doc.Row, &doc.Ident, &doc.Title, &doc.Created, &doc.Body, &doc.Category, &doc.Sample); err != nil {
  490. return nil, err
  491. }
  492. docs = append(docs, doc)
  493. }
  494. if err := rows.Err(); err != nil {
  495. return nil, err
  496. }
  497. return docs, nil
  498. }
  499. /*
  500. get image data from the images table
  501. :param id: the serial identifier of the post
  502. */
  503. func (s *SQLiteRepo) imageMetadata(id Identifier) (Image, error) {
  504. if err := validateImageIdentifier(id); err != nil {
  505. return Image{}, ErrNotExists
  506. }
  507. row := s.db.QueryRow("SELECT id, title, desc, created, category, COALESCE(media_type, '') FROM images WHERE id = ?", id)
  508. var img Image
  509. if err := row.Scan(&img.Ident, &img.Title, &img.Desc, &img.Created, &img.Category, &img.MediaType); err != nil {
  510. if errors.Is(err, sql.ErrNoRows) {
  511. return Image{}, ErrNotExists
  512. }
  513. return Image{}, err
  514. }
  515. return img, nil
  516. }
  517. func (s *SQLiteRepo) GetImage(id Identifier) (Image, error) {
  518. img, err := s.imageMetadata(id)
  519. if err != nil {
  520. return Image{}, err
  521. }
  522. img.Data, err = s.imageIO.Get(id)
  523. if err != nil {
  524. return Image{}, err
  525. }
  526. return img, nil
  527. }
  528. type memoryImage struct{ *bytes.Reader }
  529. func (memoryImage) Close() error { return nil }
  530. // OpenImage returns metadata and a seekable stream. Legacy ImageIO implementations
  531. // remain usable through a buffered fallback; callers must close the stream.
  532. func (s *SQLiteRepo) OpenImage(id Identifier) (Image, ReadSeekCloser, error) {
  533. img, err := s.imageMetadata(id)
  534. if err != nil {
  535. return Image{}, nil, err
  536. }
  537. if opener, ok := s.imageIO.(ImageOpener); ok {
  538. file, err := opener.Open(id)
  539. if err != nil {
  540. return Image{}, nil, err
  541. }
  542. return img, file, nil
  543. }
  544. data, err := s.imageIO.Get(id)
  545. if err != nil {
  546. return Image{}, nil, err
  547. }
  548. return img, memoryImage{bytes.NewReader(data)}, nil
  549. }
  550. /*
  551. Get all of the images from the datastore
  552. */
  553. func (s *SQLiteRepo) GetAllImages() []Image {
  554. imgs, err := s.GetAllImagesWithError()
  555. if err != nil {
  556. log.Printf("get all images: %v", err)
  557. }
  558. return imgs
  559. }
  560. func (s *SQLiteRepo) GetAllImagesWithError() ([]Image, error) {
  561. return s.listImages("SELECT id, title, desc, created, category, COALESCE(media_type, '') FROM images")
  562. }
  563. func (s *SQLiteRepo) GetImagesByCategory(category string) []Image {
  564. imgs, err := s.GetImagesByCategoryWithError(category)
  565. if err != nil {
  566. log.Printf("get images by category: %v", err)
  567. }
  568. return imgs
  569. }
  570. func (s *SQLiteRepo) GetImagesByCategoryWithError(category string) ([]Image, error) {
  571. return s.listImages("SELECT id, title, desc, created, category, COALESCE(media_type, '') FROM images WHERE category = ?", category)
  572. }
  573. // listImages deliberately returns metadata only, even if a media file is missing.
  574. func (s *SQLiteRepo) listImages(query string, args ...interface{}) ([]Image, error) {
  575. rows, err := s.db.Query(query, args...)
  576. if err != nil {
  577. return nil, err
  578. }
  579. defer rows.Close()
  580. imgs := []Image{}
  581. for rows.Next() {
  582. var img Image
  583. if err := rows.Scan(&img.Ident, &img.Title, &img.Desc, &img.Created, &img.Category, &img.MediaType); err != nil {
  584. return nil, err
  585. }
  586. imgs = append(imgs, img)
  587. }
  588. if err := rows.Err(); err != nil {
  589. return nil, err
  590. }
  591. return imgs, nil
  592. }
  593. /*
  594. Add an image to the database
  595. :param title: the title of the image
  596. :param location: the location to save the image to
  597. :param desc: the description of the image, if any
  598. :param data: the binary data for the image
  599. */
  600. func (s *SQLiteRepo) AddImage(data []byte, title, desc, category, mediaType string) (Identifier, error) {
  601. id := newIdentifier()
  602. err := s.imageIO.Put(data, id)
  603. if err != nil {
  604. return Identifier(""), err
  605. }
  606. _, err = s.db.Exec("INSERT INTO images (id, title, desc, created, category, media_type) VALUES (?,?,?,?,?,?)", string(id), title, desc, time.Now().String(), category, mediaType)
  607. if err != nil {
  608. return Identifier(""), err
  609. }
  610. return id, nil
  611. }
  612. // UpdateDigitalArt changes only the title and description of a digital art
  613. // record. Other image categories and the image file are never modified.
  614. func (s *SQLiteRepo) UpdateDigitalArt(id Identifier, title, desc string) error {
  615. if err := validateImageIdentifier(id); err != nil {
  616. return err
  617. }
  618. res, err := s.db.Exec("UPDATE images SET title = ?, desc = ? WHERE id = ? AND category = ? AND (SELECT COUNT(*) FROM images WHERE id = ?) = 1", title, desc, id, DIGITAL_ART, id)
  619. if err != nil {
  620. return err
  621. }
  622. affected, err := res.RowsAffected()
  623. if err != nil {
  624. return err
  625. }
  626. if affected != 1 {
  627. return ErrNotExists
  628. }
  629. return nil
  630. }
  631. // ArchiveDigitalArt removes an art listing while retaining its media and ID for
  632. // existing references. Ambiguous IDs are left untouched, regardless of category.
  633. func (s *SQLiteRepo) ArchiveDigitalArt(id Identifier) error {
  634. if err := validateImageIdentifier(id); err != nil {
  635. return err
  636. }
  637. res, err := s.db.Exec("UPDATE images SET category = ? WHERE id = ? AND category = ? AND (SELECT COUNT(*) FROM images WHERE id = ?) = 1", MISC_ASSET, id, DIGITAL_ART, id)
  638. if err != nil {
  639. return err
  640. }
  641. affected, err := res.RowsAffected()
  642. if err != nil {
  643. return err
  644. }
  645. if affected != 1 {
  646. return ErrNotExists
  647. }
  648. return nil
  649. }
  650. /*
  651. Updates a document in the database with the supplied. Only changes the title, the body, category. Keys off of the documents Identifier
  652. :param doc: the Document to upload into the database
  653. */
  654. func (s *SQLiteRepo) UpdateDocument(doc Document) error {
  655. tx, err := s.db.Begin()
  656. if err != nil {
  657. return err
  658. }
  659. stmt, err := tx.Prepare("UPDATE posts SET title = ?, body = ?, category = ?, sample = ? WHERE id = ?;")
  660. if err != nil {
  661. tx.Rollback()
  662. return err
  663. }
  664. res, err := stmt.Exec(doc.Title, doc.Body, doc.Category, doc.MakeSample(), doc.Ident)
  665. if err != nil {
  666. tx.Rollback()
  667. return err
  668. }
  669. affected, _ := res.RowsAffected()
  670. if affected != 1 {
  671. return ErrNotExists
  672. }
  673. tx.Commit()
  674. return nil
  675. }
  676. /*
  677. Adds a LinkPair to the menu database table
  678. :param item: the LinkPair to upload
  679. */
  680. func (s *SQLiteRepo) AddMenuItem(item LinkPair) error {
  681. tx, err := s.db.Begin()
  682. if err != nil {
  683. return err
  684. }
  685. fmt.Printf("from AddMenuItem: %+v\n", item)
  686. foundItem, found := s.GetMenuItemByName(item.Link, item.Text)
  687. if found {
  688. fmt.Printf("from error in AddMenuItem: %+v\n", foundItem)
  689. tx.Rollback()
  690. return ErrDuplicate
  691. }
  692. stmt, _ := tx.Prepare("INSERT INTO menu(link, text) VALUES (?,?)")
  693. _, err = stmt.Exec(item.Link, item.Text)
  694. if err != nil {
  695. tx.Rollback()
  696. return err
  697. }
  698. tx.Commit()
  699. return nil
  700. }
  701. /*
  702. Adds an item to the navbar database table
  703. :param item: the NavBarItem to upload
  704. */
  705. func (s *SQLiteRepo) AddNavbarItem(item NavBarItem) error {
  706. tx, err := s.db.Begin()
  707. if err != nil {
  708. return err
  709. }
  710. _, found := s.GetNavbarLink(item.Link, item.Redirect)
  711. if found {
  712. tx.Rollback()
  713. return ErrDuplicate
  714. }
  715. stmt, err := tx.Prepare("INSERT INTO navbar(png, link, redirect) VALUES (?,?,?)")
  716. if err != nil {
  717. tx.Rollback()
  718. return err
  719. }
  720. _, err = stmt.Exec(item.Png, item.Link, item.Redirect)
  721. if err != nil {
  722. tx.Rollback()
  723. return err
  724. }
  725. tx.Commit()
  726. return nil
  727. }
  728. /*
  729. Adds an asset to the asset database table asset
  730. :param name: the name of the asset (filename)
  731. :param data: the byte array of the PNG to upload TODO: limit this to 256kb
  732. */
  733. func (s *SQLiteRepo) AddAsset(name string, data []byte) error {
  734. tx, err := s.db.Begin()
  735. if err != nil {
  736. return err
  737. }
  738. _, found := s.GetAsset(name)
  739. if found {
  740. tx.Rollback()
  741. return ErrDuplicate
  742. }
  743. stmt, _ := tx.Prepare("INSERT INTO assets(name, data) VALUES (?,?)")
  744. _, err = stmt.Exec(name, data)
  745. if err != nil {
  746. tx.Rollback()
  747. return err
  748. }
  749. tx.Commit()
  750. return nil
  751. }
  752. /*
  753. Adds a document to the database (for text posts)
  754. :param doc: the Document to add
  755. */
  756. func (s *SQLiteRepo) AddDocument(doc Document) (Identifier, error) {
  757. id := newIdentifier()
  758. tx, err := s.db.Begin()
  759. if err != nil {
  760. return Identifier(""), err
  761. }
  762. stmt, _ := tx.Prepare("INSERT INTO posts(id, title, created, body, category, sample) VALUES (?,?,?,?,?,?)")
  763. _, err = stmt.Exec(id, doc.Title, doc.Created, doc.Body, doc.Category, doc.MakeSample())
  764. if err != nil {
  765. tx.Rollback()
  766. return Identifier(""), err
  767. }
  768. tx.Commit()
  769. return id, nil
  770. }
  771. /*
  772. Add an entry to the 'admin' table in the database
  773. :param item: an admin table k/v text to redirect pair
  774. :param tableName: the name of the table to populate the link in on the UI
  775. */
  776. func (s *SQLiteRepo) AddAdminTableEntry(item TableData, category string) error {
  777. tx, err := s.db.Begin()
  778. if err != nil {
  779. return err
  780. }
  781. _, found := s.GetAdminTableEntry(item.DisplayName, item.Link, category)
  782. if found {
  783. tx.Rollback()
  784. return ErrDuplicate
  785. }
  786. stmt, _ := tx.Prepare("INSERT INTO admin (display_name, link, category) VALUES (?,?,?)")
  787. _, err = stmt.Exec(item.DisplayName, item.Link, category)
  788. if err != nil {
  789. tx.Rollback()
  790. return err
  791. }
  792. tx.Commit()
  793. return nil
  794. }
  795. /*
  796. Delete a document from the db
  797. :param id: the identifier of the document to remove
  798. */
  799. func (s *SQLiteRepo) DeleteDocument(id Identifier) error {
  800. tx, err := s.db.Begin()
  801. if err != nil {
  802. return err
  803. }
  804. stmt, _ := tx.Prepare("DELETE FROM posts WHERE id=?")
  805. _, err = stmt.Exec(id)
  806. if err != nil {
  807. tx.Rollback()
  808. return err
  809. }
  810. tx.Commit()
  811. return nil
  812. }
  813. /*
  814. Delete navigation bar item
  815. :param id: the name or 'id' of the navbar item to remove
  816. */
  817. func (s *SQLiteRepo) DeleteNavbarItem(id Identifier) error {
  818. tx, err := s.db.Begin()
  819. if err != nil {
  820. return err
  821. }
  822. stmt, _ := tx.Prepare("DELETE FROM navbar WHERE link=?")
  823. result, err := stmt.Exec(id)
  824. if err != nil {
  825. tx.Rollback()
  826. return err
  827. }
  828. rowsAffected, _ := result.RowsAffected()
  829. if rowsAffected < 1 {
  830. return ErrNotExists
  831. }
  832. tx.Commit()
  833. return nil
  834. }
  835. // Get all Hosts from the host table
  836. func (s *SQLiteRepo) AllDocuments() []Document {
  837. docs, err := s.AllDocumentsWithError()
  838. if err != nil {
  839. log.Printf("get all documents: %v", err)
  840. }
  841. return docs
  842. }
  843. func (s *SQLiteRepo) AllDocumentsWithError() ([]Document, error) {
  844. return s.listDocuments("SELECT * FROM posts")
  845. }
  846. type InvalidSkipArg struct{ Skip int }
  847. func (i *InvalidSkipArg) Error() string {
  848. return fmt.Sprintf("Invalid skip amount was passed: %v", i.Skip)
  849. }
  850. type ImageStoreItem struct {
  851. Identifier string `json:"identifier"`
  852. Filename string `json:"filename"`
  853. AbsolutePath string `json:"absolute_path"`
  854. Title string `json:"title" form:"title"`
  855. Created string `json:"created"`
  856. Desc string `json:"description" form:"description"`
  857. Category string `json:"category"`
  858. ApiPath string
  859. }
  860. /*
  861. Function to return the location of the image store. Wrapping the env call in
  862. a function so that refactoring is easier
  863. */
  864. func GetImageStore() string {
  865. return os.Getenv(env.IMAGE_STORE)
  866. }
  867. // Wrapping the new id call in a function to make refactoring easier
  868. func newIdentifier() Identifier {
  869. return Identifier(uuid.NewString())
  870. }