storage.go 14 KB

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