storage.go 14 KB

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