storage.go 17 KB

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