storage.go 16 KB

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