storage.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. package helpers
  2. import (
  3. "database/sql"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log"
  8. "mime/multipart"
  9. "os"
  10. "path"
  11. "strings"
  12. "time"
  13. "git.aetherial.dev/aeth/keiji/pkg/env"
  14. "github.com/google/uuid"
  15. "github.com/redis/go-redis/v9"
  16. )
  17. type DatabaseSchema struct {
  18. // Gotta figure out what this looks like
  19. // so that the ExtractAll() function gets
  20. // all of the data from the database
  21. }
  22. type MenuLinkPair struct {
  23. MenuLink string `json:"link"`
  24. LinkText string `json:"text"`
  25. }
  26. type NavBarItem struct {
  27. Png []byte `json:"png"`
  28. Link string `json:"link"`
  29. Redirect string `json:"redirect"`
  30. }
  31. type Asset struct {
  32. Name string
  33. Data []byte
  34. }
  35. type Identifier string
  36. type Document struct {
  37. Row int
  38. Ident Identifier `json:"id"`
  39. Title string `json:"title"`
  40. Created string `json:"created"`
  41. Body string `json:"body"`
  42. Category string `json:"category"`
  43. Sample string `json:"sample"`
  44. }
  45. /*
  46. Truncates a text post into a 256 character long 'sample' for displaying posts
  47. */
  48. func (d *Document) MakeSample() string {
  49. t := strings.Split(d.Body, "")
  50. var sample []string
  51. if len(d.Body) < 256 {
  52. return d.Body
  53. }
  54. for i := 0; i < 256; i++ {
  55. sample = append(sample, t[i])
  56. }
  57. sample = append(sample, " ...")
  58. return strings.Join(sample, "")
  59. }
  60. type Image struct {
  61. Ident Identifier `json:"identifier"`
  62. Location string `json:"title" form:"title"`
  63. Title string `json:"description" form:"description"`
  64. File *multipart.FileHeader `form:"file"`
  65. Desc string
  66. Created string
  67. Category string
  68. Data []byte
  69. }
  70. type DocumentIO interface {
  71. GetDocument(id Identifier) (Document, error)
  72. GetImage(id Identifier) (Image, error)
  73. GetAllImages() []Image
  74. UpdateDocument(doc Document) error
  75. DeleteDocument(id Identifier) error
  76. AddDocument(doc Document) error
  77. AddImage(data []byte, title, desc string) error
  78. AddAdminTableEntry(TableData, string) error
  79. AddNavbarItem(NavBarItem) error
  80. AddMenuItem(MenuLinkPair) error
  81. GetByCategory(category string) []Document
  82. AllDocuments() []Document
  83. GetDropdownElements() []MenuLinkPair
  84. GetNavBarLinks() []NavBarItem
  85. GetAssets() []Asset
  86. GetAdminTables() AdminPage
  87. }
  88. var (
  89. ErrDuplicate = errors.New("record already exists")
  90. ErrNotExists = errors.New("row not exists")
  91. ErrUpdateFailed = errors.New("update failed")
  92. ErrDeleteFailed = errors.New("delete failed")
  93. )
  94. type SQLiteRepo struct {
  95. db *sql.DB
  96. }
  97. // Instantiate a new SQLiteRepo struct
  98. func NewSQLiteRepo(db *sql.DB) *SQLiteRepo {
  99. return &SQLiteRepo{
  100. db: db,
  101. }
  102. }
  103. // Creates a new SQL table for text posts
  104. func (r *SQLiteRepo) Migrate() error {
  105. postsTable := `
  106. CREATE TABLE IF NOT EXISTS posts(
  107. row INTEGER PRIMARY KEY AUTOINCREMENT,
  108. id TEXT NOT NULL UNIQUE,
  109. title TEXT NOT NULL,
  110. created TEXT NOT NULL,
  111. body TEXT NOT NULL,
  112. category TEXT NOT NULL,
  113. sample TEXT NOT NULL
  114. );
  115. `
  116. imagesTable := `
  117. CREATE TABLE IF NOT EXISTS images(
  118. row INTEGER PRIMARY KEY AUTOINCREMENT,
  119. id TEXT NOT NULL,
  120. title TEXT NOT NULL,
  121. location TEXT NOT NULL,
  122. desc TEXT NOT NULL,
  123. created TEXT NOT NULL
  124. );
  125. `
  126. menuItemsTable := `
  127. CREATE TABLE IF NOT EXISTS menu(
  128. row INTEGER PRIMARY KEY AUTOINCREMENT,
  129. link TEXT NOT NULL,
  130. text TEXT NOT NULL
  131. );
  132. `
  133. navbarItemsTable := `
  134. CREATE TABLE IF NOT EXISTS navbar(
  135. row INTEGER PRIMARY KEY AUTOINCREMENT,
  136. png BLOB NOT NULL,
  137. link TEXT NOT NULL,
  138. redirect TEXT
  139. );`
  140. assetTable := `
  141. CREATE TABLE IF NOT EXISTS assets(
  142. row INTEGER PRIMARY KEY AUTOINCREMENT,
  143. name TEXT NOT NULL,
  144. data BLOB NOT NULL
  145. );
  146. `
  147. adminTable := `
  148. CREATE TABLE IF NOT EXISTS admin(
  149. row INTEGER PRIMARY KEY AUTOINCREMENT,
  150. display_name TEXT NOT NULL,
  151. link TEXT NOT NULL,
  152. category TEXT NOT NULL
  153. );
  154. `
  155. seedQueries := []string{postsTable, imagesTable, menuItemsTable, navbarItemsTable, assetTable, adminTable}
  156. for i := range seedQueries {
  157. _, err := r.db.Exec(seedQueries[i])
  158. if err != nil {
  159. return err
  160. }
  161. }
  162. return nil
  163. }
  164. /*
  165. Seed the database with the necessary configuration items to function properly
  166. :param menu: the text file containing the k/v pair for the navigation menu
  167. :param pngs: the text file containing the k/v pair for the icon names -> redirect links
  168. :param dir: the directory that the PNG assets are in (note: the k/v pair in pngs will read from this dir)
  169. */
  170. func (s *SQLiteRepo) Seed(menu string, pngs string, dir string) { // TODO: make a bootstrap file with a comprehensive unmarshalling sequence for tighter control of the seeing procedute
  171. b, err := os.ReadFile(menu)
  172. if err != nil {
  173. log.Fatal(err)
  174. }
  175. entries := strings.Split(string(b), "\n")
  176. for i := range entries {
  177. if entries[i] == "" {
  178. continue
  179. }
  180. info := strings.Split(entries[i], "=")
  181. err := s.AddMenuItem(MenuLinkPair{MenuLink: info[0], LinkText: info[1]})
  182. if err != nil {
  183. log.Fatal(err)
  184. }
  185. }
  186. b, err = os.ReadFile(pngs)
  187. if err != nil {
  188. log.Fatal(err)
  189. }
  190. entries = strings.Split(string(b), "\n")
  191. for i := range entries {
  192. if entries[i] == "" {
  193. continue
  194. }
  195. info := strings.Split(entries[i], "=")
  196. b, err := os.ReadFile(path.Join(dir, info[0]))
  197. if err != nil {
  198. log.Fatal(err)
  199. }
  200. err = s.AddNavbarItem(NavBarItem{Png: b, Link: info[0], Redirect: info[1]})
  201. if err != nil {
  202. log.Fatal(err)
  203. }
  204. }
  205. assets, err := os.ReadDir(dir)
  206. if err != nil {
  207. log.Fatal(err)
  208. }
  209. for i := range assets {
  210. b, err := os.ReadFile(path.Join(dir, assets[i].Name()))
  211. if err != nil {
  212. log.Fatal(err)
  213. }
  214. err = s.AddAsset(assets[i].Name(), b)
  215. if err != nil {
  216. log.Fatal(err)
  217. }
  218. }
  219. }
  220. /*
  221. Get all dropdown menu elements. Returns a list of MenuLinkPair structs with the text and redirect location
  222. */
  223. func (s *SQLiteRepo) GetDropdownElements() []MenuLinkPair {
  224. rows, err := s.db.Query("SELECT * FROM menu")
  225. var menuItems []MenuLinkPair
  226. defer rows.Close()
  227. for rows.Next() {
  228. var id int
  229. var item MenuLinkPair
  230. err = rows.Scan(&id, &item.MenuLink, &item.LinkText)
  231. if err != nil {
  232. log.Fatal(err)
  233. }
  234. menuItems = append(menuItems, item)
  235. }
  236. return menuItems
  237. }
  238. /*
  239. 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
  240. */
  241. func (s *SQLiteRepo) GetNavBarLinks() []NavBarItem {
  242. rows, err := s.db.Query("SELECT * FROM navbar")
  243. var navbarItems []NavBarItem
  244. defer rows.Close()
  245. for rows.Next() {
  246. var item NavBarItem
  247. var id int
  248. err = rows.Scan(&id, &item.Png, &item.Link, &item.Redirect)
  249. if err != nil {
  250. log.Fatal(err)
  251. }
  252. navbarItems = append(navbarItems, item)
  253. }
  254. return navbarItems
  255. }
  256. /*
  257. get all assets from the asset table
  258. */
  259. func (s *SQLiteRepo) GetAssets() []Asset {
  260. rows, err := s.db.Query("SELECT * FROM assets")
  261. var assets []Asset
  262. defer rows.Close()
  263. for rows.Next() {
  264. var item Asset
  265. var id int
  266. err = rows.Scan(&id, &item.Name, &item.Data)
  267. if err != nil {
  268. log.Fatal(err)
  269. }
  270. assets = append(assets, item)
  271. }
  272. return assets
  273. }
  274. /*
  275. get all assets from the asset table
  276. */
  277. func (s *SQLiteRepo) GetAdminTables() AdminPage {
  278. rows, err := s.db.Query("SELECT * FROM admin")
  279. adminPage := AdminPage{Tables: map[string][]TableData{}}
  280. defer rows.Close()
  281. for rows.Next() {
  282. var item TableData
  283. var id int
  284. var category string
  285. err = rows.Scan(&id, &item.DisplayName, &item.Link, &category)
  286. if err != nil {
  287. log.Fatal(err)
  288. }
  289. adminPage.Tables[category] = append(adminPage.Tables[category], item)
  290. }
  291. return adminPage
  292. }
  293. /*
  294. Retrieve a document from the sqlite db
  295. :param id: the Identifier of the post
  296. */
  297. func (s *SQLiteRepo) GetDocument(id Identifier) (Document, error) {
  298. row := s.db.QueryRow("SELECT * FROM posts WHERE id = ?", id)
  299. var post Document
  300. var rowNum int
  301. if err := row.Scan(&rowNum, &post.Ident, &post.Title, &post.Created, &post.Body, &post.Category, &post.Sample); err != nil {
  302. if errors.Is(err, sql.ErrNoRows) {
  303. return post, ErrNotExists
  304. }
  305. return post, err
  306. }
  307. return post, nil
  308. }
  309. /*
  310. Get all documents by category
  311. :param category: the category to retrieve all docs from
  312. */
  313. func (s *SQLiteRepo) GetByCategory(category string) []Document {
  314. rows, err := s.db.Query("SELECT * FROM posts WHERE category = ?", category)
  315. if err != nil {
  316. log.Fatal(err)
  317. }
  318. var docs []Document
  319. defer rows.Close()
  320. for rows.Next() {
  321. var doc Document
  322. err := rows.Scan(&doc.Row, &doc.Ident, &doc.Title, &doc.Created, &doc.Body, &doc.Category, &doc.Sample)
  323. if err != nil {
  324. log.Fatal(err)
  325. }
  326. docs = append(docs, doc)
  327. }
  328. err = rows.Err()
  329. if err != nil {
  330. log.Fatal(err)
  331. }
  332. return docs
  333. }
  334. /*
  335. get image data from the images table
  336. :param id: the serial identifier of the post
  337. */
  338. func (s *SQLiteRepo) GetImage(id Identifier) (Image, error) {
  339. row := s.db.QueryRow("SELECT * FROM images WHERE id = ?", id)
  340. var rowNum int
  341. var title string
  342. var location string
  343. var desc string
  344. var created string
  345. if err := row.Scan(&rowNum, &title, &location, &desc, &created); err != nil {
  346. if errors.Is(err, sql.ErrNoRows) {
  347. return Image{}, ErrNotExists
  348. }
  349. return Image{}, err
  350. }
  351. data, err := os.ReadFile(location)
  352. if err != nil {
  353. return Image{}, err
  354. }
  355. return Image{Ident: id, Title: title, Location: location, Desc: desc, Data: data, Created: created}, nil
  356. }
  357. /*
  358. Get all of the images from the datastore
  359. */
  360. func (s *SQLiteRepo) GetAllImages() []Image {
  361. rows, err := s.db.Query("SELECT * FROM images")
  362. if err != nil {
  363. log.Fatal(err)
  364. }
  365. imgs := []Image{}
  366. for rows.Next() {
  367. var img Image
  368. var rowNum int
  369. err := rows.Scan(&rowNum, &img.Ident, &img.Title, &img.Location, &img.Desc, &img.Created)
  370. if err != nil {
  371. log.Fatal(err)
  372. }
  373. b, err := os.ReadFile(img.Location)
  374. if err != nil {
  375. log.Fatal(err)
  376. }
  377. imgs = append(imgs, Image{Ident: img.Ident, Title: img.Title, Location: img.Location, Desc: img.Desc, Data: b, Created: img.Created})
  378. }
  379. err = rows.Err()
  380. if err != nil {
  381. log.Fatal(err)
  382. }
  383. return imgs
  384. }
  385. /*
  386. Add an image to the database
  387. :param title: the title of the image
  388. :param location: the location to save the image to
  389. :param desc: the description of the image, if any
  390. :param data: the binary data for the image
  391. */
  392. func (s *SQLiteRepo) AddImage(data []byte, title string, desc string) error {
  393. id := newIdentifier()
  394. fsLoc := path.Join(GetImageStore(), string(id))
  395. err := os.WriteFile(fsLoc, data, os.ModePerm)
  396. if err != nil {
  397. return err
  398. }
  399. _, err = s.db.Exec("INSERT INTO images (id, title, location, desc, created) VALUES (?,?,?,?,?)", string(id), title, fsLoc, desc, time.Now().String())
  400. if err != nil {
  401. return err
  402. }
  403. return nil
  404. }
  405. /*
  406. Updates a document in the database with the supplied. Only changes the title, the body, category. Keys off of the documents Identifier
  407. :param doc: the Document to upload into the database
  408. */
  409. func (s *SQLiteRepo) UpdateDocument(doc Document) error {
  410. tx, err := s.db.Begin()
  411. if err != nil {
  412. return err
  413. }
  414. stmt, err := tx.Prepare("UPDATE posts SET title = ?, body = ?, category = ?, sample = ? WHERE id = ?;")
  415. if err != nil {
  416. tx.Rollback()
  417. return err
  418. }
  419. _, err = stmt.Exec(doc.Title, doc.Body, doc.Category, doc.MakeSample(), doc.Ident)
  420. if err != nil {
  421. tx.Rollback()
  422. return err
  423. }
  424. tx.Commit()
  425. return nil
  426. }
  427. /*
  428. Adds a MenuLinkPair to the menu database table
  429. :param item: the MenuLinkPair to upload
  430. */
  431. func (s *SQLiteRepo) AddMenuItem(item MenuLinkPair) error {
  432. tx, err := s.db.Begin()
  433. if err != nil {
  434. return err
  435. }
  436. stmt, _ := tx.Prepare("INSERT INTO menu(link, text) VALUES (?,?)")
  437. _, err = stmt.Exec(item.MenuLink, item.LinkText)
  438. if err != nil {
  439. tx.Rollback()
  440. return err
  441. }
  442. tx.Commit()
  443. return nil
  444. }
  445. /*
  446. Adds an item to the navbar database table
  447. :param item: the NavBarItem to upload
  448. */
  449. func (s *SQLiteRepo) AddNavbarItem(item NavBarItem) error {
  450. tx, err := s.db.Begin()
  451. if err != nil {
  452. return err
  453. }
  454. stmt, err := tx.Prepare("INSERT INTO navbar(png, link, redirect) VALUES (?,?,?)")
  455. if err != nil {
  456. tx.Rollback()
  457. return err
  458. }
  459. _, err = stmt.Exec(item.Png, item.Link, item.Redirect)
  460. if err != nil {
  461. tx.Rollback()
  462. return err
  463. }
  464. err = s.AddAsset(item.Link, item.Png)
  465. if err != nil {
  466. tx.Rollback()
  467. return err
  468. }
  469. tx.Commit()
  470. return nil
  471. }
  472. /*
  473. Adds an asset to the asset database table asset
  474. :param name: the name of the asset (filename)
  475. :param data: the byte array of the PNG to upload TODO: limit this to 256kb
  476. */
  477. func (s *SQLiteRepo) AddAsset(name string, data []byte) error {
  478. tx, err := s.db.Begin()
  479. if err != nil {
  480. return err
  481. }
  482. stmt, _ := tx.Prepare("INSERT INTO assets(name, data) VALUES (?,?)")
  483. _, err = stmt.Exec(name, data)
  484. if err != nil {
  485. tx.Rollback()
  486. return err
  487. }
  488. tx.Commit()
  489. return nil
  490. }
  491. /*
  492. Adds a document to the database (for text posts)
  493. :param doc: the Document to add
  494. */
  495. func (s *SQLiteRepo) AddDocument(doc Document) error {
  496. id := uuid.New()
  497. tx, err := s.db.Begin()
  498. if err != nil {
  499. return err
  500. }
  501. stmt, _ := tx.Prepare("INSERT INTO posts(id, title, created, body, category, sample) VALUES (?,?,?,?,?,?)")
  502. _, err = stmt.Exec(id.String(), doc.Title, doc.Created, doc.Body, doc.Category, doc.MakeSample())
  503. if err != nil {
  504. tx.Rollback()
  505. return err
  506. }
  507. tx.Commit()
  508. return nil
  509. }
  510. /*
  511. Add an entry to the 'admin' table in the database
  512. :param item: an admin table k/v text to redirect pair
  513. :param tableName: the name of the table to populate the link in on the UI
  514. */
  515. func (s *SQLiteRepo) AddAdminTableEntry(item TableData, category string) error {
  516. tx, err := s.db.Begin()
  517. if err != nil {
  518. return err
  519. }
  520. stmt, _ := tx.Prepare("INSERT INTO admin (display_name, link, category) VALUES (?,?,?)")
  521. _, err = stmt.Exec(item.DisplayName, item.Link, category)
  522. if err != nil {
  523. tx.Rollback()
  524. return err
  525. }
  526. tx.Commit()
  527. return nil
  528. }
  529. /*
  530. Delete a document from the db
  531. :param id: the identifier of the document to remove
  532. */
  533. func (s *SQLiteRepo) DeleteDocument(id Identifier) error {
  534. tx, err := s.db.Begin()
  535. if err != nil {
  536. return err
  537. }
  538. stmt, _ := tx.Prepare("DELETE FROM posts WHERE id=?")
  539. _, err = stmt.Exec(id)
  540. if err != nil {
  541. tx.Rollback()
  542. return err
  543. }
  544. tx.Commit()
  545. return nil
  546. }
  547. // Get all Hosts from the host table
  548. func (s *SQLiteRepo) AllDocuments() []Document {
  549. rows, err := s.db.Query("SELECT * FROM posts")
  550. if err != nil {
  551. fmt.Printf("There was an issue getting all posts. %s", err.Error())
  552. return nil
  553. }
  554. defer rows.Close()
  555. all := []Document{}
  556. for rows.Next() {
  557. var post Document
  558. if err := rows.Scan(&post.Ident, &post.Title, &post.Created, &post.Body, &post.Sample); err != nil {
  559. fmt.Printf("There was an error getting all documents. %s", err.Error())
  560. return nil
  561. }
  562. all = append(all, post)
  563. }
  564. return all
  565. }
  566. type InvalidSkipArg struct{ Skip int }
  567. func (i *InvalidSkipArg) Error() string {
  568. return fmt.Sprintf("Invalid skip amount was passed: %v", i.Skip)
  569. }
  570. type ImageStoreItem struct {
  571. Identifier string `json:"identifier"`
  572. Filename string `json:"filename"`
  573. AbsolutePath string `json:"absolute_path"`
  574. Title string `json:"title" form:"title"`
  575. Created string `json:"created"`
  576. Desc string `json:"description" form:"description"`
  577. Category string `json:"category"`
  578. ApiPath string
  579. }
  580. /*
  581. Create a new ImageStoreItem
  582. :param fname: the name of the file to be saved
  583. :param title: the canonical title to give the image
  584. :param desc: the description to associate to the image
  585. */
  586. func NewImageStoreItem(title string, desc string) Image {
  587. id := newIdentifier()
  588. img := Image{
  589. Ident: id,
  590. Title: title,
  591. Category: DIGITAL_ART,
  592. Location: GetImageStore(),
  593. Created: time.Now().UTC().String(),
  594. Desc: desc,
  595. }
  596. return img
  597. }
  598. /*
  599. Function to return the location of the image store. Wrapping the env call in
  600. a function so that refactoring is easier
  601. */
  602. func GetImageStore() string {
  603. return os.Getenv(env.IMAGE_STORE)
  604. }
  605. // Wrapping the new id call in a function to make refactoring easier
  606. func newIdentifier() Identifier {
  607. return Identifier(uuid.NewString())
  608. }
  609. /*
  610. Return database entries of the images that exist in the imagestore
  611. :param rds: pointer to a RedisCaller to perform the lookups with
  612. */
  613. func GetImageData(rds *RedisCaller) ([]*ImageStoreItem, error) {
  614. ids, err := rds.GetByCategory(DIGITAL_ART)
  615. if err != nil {
  616. return nil, err
  617. }
  618. var imageEntries []*ImageStoreItem
  619. for i := range ids {
  620. val, err := rds.Client.Get(rds.ctx, ids[i]).Result()
  621. if err == redis.Nil {
  622. return nil, err
  623. } else if err != nil {
  624. return nil, err
  625. }
  626. data := []byte(val)
  627. var imageEntry ImageStoreItem
  628. err = json.Unmarshal(data, &imageEntry)
  629. if err != nil {
  630. return nil, err
  631. }
  632. imageEntry.ApiPath = fmt.Sprintf("/api/v1/images/%s", imageEntry.Filename)
  633. imageEntries = append(imageEntries, &imageEntry)
  634. }
  635. return imageEntries, err
  636. }