storage.go 16 KB

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