storage.go 15 KB

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