storage.go 16 KB

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