storage.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. package helpers
  2. import (
  3. "database/sql"
  4. "encoding/json"
  5. "log"
  6. "path"
  7. "errors"
  8. "fmt"
  9. "os"
  10. "strings"
  11. "time"
  12. "git.aetherial.dev/aeth/keiji/pkg/env"
  13. "github.com/google/uuid"
  14. "github.com/redis/go-redis/v9"
  15. )
  16. type DatabaseSchema struct {
  17. // Gotta figure out what this looks like
  18. // so that the ExtractAll() function gets
  19. // all of the data from the database
  20. }
  21. type MenuLinkPair struct {
  22. MenuLink string `json:"link"`
  23. LinkText string `json:"text"`
  24. }
  25. type NavBarItem struct {
  26. Png []byte `json:"png"`
  27. Link string `json:"link"`
  28. Redirect string `json:"redirect"`
  29. }
  30. type Asset struct {
  31. Name string
  32. Data []byte
  33. }
  34. type Identifier string
  35. type Document struct {
  36. Row int
  37. Ident string `json:"id"`
  38. Title string `json:"title"`
  39. Created string `json:"created"`
  40. Body string `json:"body"`
  41. Category string `json:"category"`
  42. Sample string `json:"sample"`
  43. }
  44. /*
  45. Truncates a text post into a 256 character long 'sample' for displaying posts
  46. */
  47. func (d *Document) MakeSample() string {
  48. t := strings.Split(d.Body, "")
  49. var sample []string
  50. if len(d.Body) < 256 {
  51. return d.Body
  52. }
  53. for i := 0; i < 256; i++ {
  54. sample = append(sample, t[i])
  55. }
  56. sample = append(sample, " ...")
  57. return strings.Join(sample, "")
  58. }
  59. type Image struct {
  60. Ident string
  61. Location string
  62. Title string
  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. UpdateDocument(doc Document) error
  72. DeleteDocument(id Identifier) error
  73. AddDocument(doc Document) error
  74. AddImage(img Image) error
  75. GetByCategory(category string) []Document
  76. AllDocuments() []Document
  77. GetDropdownElements() []MenuLinkPair
  78. GetNavBarLinks() []NavBarItem
  79. GetAssets() []Asset
  80. }
  81. var (
  82. ErrDuplicate = errors.New("record already exists")
  83. ErrNotExists = errors.New("row not exists")
  84. ErrUpdateFailed = errors.New("update failed")
  85. ErrDeleteFailed = errors.New("delete failed")
  86. )
  87. type SQLiteRepo struct {
  88. db *sql.DB
  89. }
  90. // Instantiate a new SQLiteRepo struct
  91. func NewSQLiteRepo(db *sql.DB) *SQLiteRepo {
  92. return &SQLiteRepo{
  93. db: db,
  94. }
  95. }
  96. // Creates a new SQL table for text posts
  97. func (r *SQLiteRepo) Migrate() error {
  98. postsTable := `
  99. CREATE TABLE IF NOT EXISTS posts(
  100. row INTEGER PRIMARY KEY AUTOINCREMENT,
  101. id TEXT NOT NULL,
  102. title TEXT NOT NULL,
  103. created TEXT NOT NULL,
  104. body TEXT NOT NULL UNIQUE,
  105. category TEXT NOT NULL,
  106. sample TEXT NOT NULL
  107. );
  108. `
  109. imagesTable := `
  110. CREATE TABLE IF NOT EXISTS images(
  111. row INTEGER PRIMARY KEY AUTOINCREMENT,
  112. id TEXT NOT NULL,
  113. title TEXT NOT NULL,
  114. location TEXT NOT NULL,
  115. description TEXT NOT NULL,
  116. created TEXT NOT NULL,
  117. category TEXT NOT NULL
  118. );
  119. `
  120. menuItemsTable := `
  121. CREATE TABLE IF NOT EXISTS menu(
  122. row INTEGER PRIMARY KEY AUTOINCREMENT,
  123. link TEXT NOT NULL,
  124. text TEXT NOT NULL
  125. );
  126. `
  127. navbarItemsTable := `
  128. CREATE TABLE IF NOT EXISTS navbar(
  129. row INTEGER PRIMARY KEY AUTOINCREMENT,
  130. png BLOB NOT NULL,
  131. link TEXT NOT NULL,
  132. redirect TEXT
  133. );`
  134. assetTable := `
  135. CREATE TABLE IF NOT EXISTS assets(
  136. row INTEGER PRIMARY KEY AUTOINCREMENT,
  137. name TEXT NOT NULL,
  138. data BLOB NOT NULL
  139. );
  140. `
  141. seedQueries := []string{postsTable, imagesTable, menuItemsTable, navbarItemsTable, assetTable}
  142. for i := range seedQueries {
  143. _, err := r.db.Exec(seedQueries[i])
  144. if err != nil {
  145. return err
  146. }
  147. }
  148. return nil
  149. }
  150. func (s *SQLiteRepo) Seed(menu string, pngs string, dir string) {
  151. b, err := os.ReadFile(menu)
  152. if err != nil {log.Fatal(err)}
  153. entries := strings.Split(string(b), "\n")
  154. for i := range entries {
  155. if entries[i] == "" {
  156. continue
  157. }
  158. info := strings.Split(entries[i], "=")
  159. err := s.AddMenuItem(MenuLinkPair{MenuLink: info[0], LinkText: info[1]})
  160. if err != nil {
  161. log.Fatal(err)
  162. }
  163. }
  164. b, err = os.ReadFile(pngs)
  165. if err != nil {log.Fatal(err)}
  166. entries = strings.Split(string(b), "\n")
  167. for i := range entries {
  168. if entries[i] == "" {continue}
  169. info := strings.Split(entries[i], "=")
  170. b, err := os.ReadFile(path.Join(dir, info[0]))
  171. if err != nil {log.Fatal(err)}
  172. err = s.AddNavbarItem(NavBarItem{Png: b, Link: info[0], Redirect: info[1]})
  173. if err != nil {log.Fatal(err)}
  174. }
  175. assets, err := os.ReadDir(dir)
  176. if err != nil {
  177. log.Fatal(err)
  178. }
  179. for i := range assets {
  180. b, err := os.ReadFile(path.Join(dir, assets[i].Name()))
  181. if err != nil {
  182. log.Fatal(err)
  183. }
  184. err = s.AddAsset(assets[i].Name(), b)
  185. if err != nil {
  186. log.Fatal(err)
  187. }
  188. }
  189. }
  190. /*
  191. Get all dropdown menu elements
  192. */
  193. func (s *SQLiteRepo) GetDropdownElements() []MenuLinkPair {
  194. rows, err := s.db.Query("SELECT * FROM menu")
  195. var menuItems []MenuLinkPair
  196. defer rows.Close()
  197. for rows.Next() {
  198. var id int
  199. var item MenuLinkPair
  200. err = rows.Scan(&id, &item.MenuLink, &item.LinkText)
  201. if err != nil {
  202. log.Fatal(err)
  203. }
  204. menuItems = append(menuItems, item)
  205. }
  206. return menuItems
  207. }
  208. /*
  209. Get all nav bar items
  210. */
  211. func (s *SQLiteRepo) GetNavBarLinks() []NavBarItem {
  212. rows, err := s.db.Query("SELECT * FROM navbar")
  213. var navbarItems []NavBarItem
  214. defer rows.Close()
  215. for rows.Next() {
  216. var item NavBarItem
  217. var id int
  218. err = rows.Scan(&id, &item.Png, &item.Link, &item.Redirect)
  219. if err != nil {
  220. log.Fatal(err)
  221. }
  222. navbarItems= append(navbarItems, item)
  223. }
  224. return navbarItems
  225. }
  226. /*
  227. get all assets from the asset table
  228. */
  229. func (s *SQLiteRepo) GetAssets() []Asset {
  230. rows, err := s.db.Query("SELECT * FROM assets")
  231. var assets []Asset
  232. defer rows.Close()
  233. for rows.Next() {
  234. var item Asset
  235. var id int
  236. err = rows.Scan(&id, &item.Name, &item.Data)
  237. if err != nil {
  238. log.Fatal(err)
  239. }
  240. assets = append(assets, item)
  241. }
  242. return assets
  243. }
  244. /*
  245. Retrieve a document from the sqlite db
  246. :param id: the Identifier of the post
  247. */
  248. func (s *SQLiteRepo) GetDocument(id Identifier) (Document, error) {
  249. row := s.db.QueryRow("SELECT * FROM posts WHERE id = ?", id)
  250. var post Document
  251. if err := row.Scan(&post); err != nil {
  252. if errors.Is(err, sql.ErrNoRows) {
  253. return post, ErrNotExists
  254. }
  255. return post, err
  256. }
  257. return post, nil
  258. }
  259. /*
  260. Get all documents by category
  261. */
  262. func (s *SQLiteRepo) GetByCategory(category string) []Document {
  263. rows, err := s.db.Query("SELECT * FROM posts WHERE category = ?", category)
  264. if err != nil {
  265. log.Fatal(err)
  266. }
  267. var docs []Document
  268. defer rows.Close()
  269. for rows.Next() {
  270. var doc Document
  271. err := rows.Scan(&doc.Row, &doc.Ident, &doc.Title, &doc.Created, &doc.Body, &doc.Category, &doc.Sample)
  272. if err != nil {
  273. log.Fatal(err)
  274. }
  275. docs = append(docs, doc)
  276. }
  277. err = rows.Err()
  278. if err != nil {
  279. log.Fatal(err)
  280. }
  281. return docs
  282. }
  283. /*
  284. get image data from the images table
  285. :param id: the serial identifier of the post
  286. */
  287. func (s *SQLiteRepo) GetImage(id Identifier) (Image, error) {
  288. row := s.db.QueryRow("SELECT * FROM images WHERE id = ?", id)
  289. var title string
  290. var location string
  291. var desc string
  292. var category string
  293. var created string
  294. if err := row.Scan(&title, &location, &desc, &created, &category); err != nil {
  295. if errors.Is(err, sql.ErrNoRows) {
  296. return Image{}, ErrNotExists
  297. }
  298. return Image{}, err
  299. }
  300. data, err := os.ReadFile(location)
  301. if err != nil {
  302. return Image{}, err
  303. }
  304. return Image{Title: title, Location: location, Desc: desc, Data: data, Created: created, Category: category}, nil
  305. }
  306. /*
  307. Add an image to the database
  308. :param title: the title of the image
  309. :param location: the location to save the image to
  310. :param desc: the description of the image, if any
  311. :param data: the binary data for the image
  312. */
  313. func (s *SQLiteRepo) AddImage(img Image) error {
  314. err := os.WriteFile(path.Join(img.Location, img.Ident), img.Data, os.ModeAppend)
  315. if err != nil {
  316. return err
  317. }
  318. _, err = s.db.Exec("INSERT INTO images (id, title, location, desc, created, category) VALUES (?,?,?,?,?,?,?)", img.Ident, img.Title, img.Location, img.Desc, img.Created, img.Category)
  319. if err != nil {
  320. return err
  321. }
  322. return nil
  323. }
  324. func (s *SQLiteRepo) UpdateDocument(doc Document) error {
  325. tx, err := s.db.Begin()
  326. if err != nil {
  327. return err
  328. }
  329. stmt,_ := tx.Prepare("UPDATE posts set title=? body=? category=? WHERE id=?")
  330. _, err = stmt.Exec(doc.Title, doc.Body, doc.Category, doc.Ident)
  331. if err != nil {
  332. tx.Rollback()
  333. return err
  334. }
  335. tx.Commit()
  336. return nil
  337. }
  338. func (s *SQLiteRepo) AddMenuItem(item MenuLinkPair) error {
  339. tx, err := s.db.Begin()
  340. if err != nil {
  341. return err
  342. }
  343. stmt,_ := tx.Prepare("INSERT INTO menu(link, text) VALUES (?,?)")
  344. _, err = stmt.Exec(item.MenuLink, item.LinkText)
  345. if err != nil {
  346. tx.Rollback()
  347. return err
  348. }
  349. tx.Commit()
  350. return nil
  351. }
  352. func (s *SQLiteRepo) AddNavbarItem(item NavBarItem) error {
  353. tx, err := s.db.Begin()
  354. if err != nil {
  355. return err
  356. }
  357. stmt,_ := tx.Prepare("INSERT INTO navbar(png, link, redirect) VALUES (?,?,?)")
  358. _, err = stmt.Exec(item.Png, item.Link, item.Redirect)
  359. if err != nil {
  360. tx.Rollback()
  361. return err
  362. }
  363. tx.Commit()
  364. return nil
  365. }
  366. func (s *SQLiteRepo) AddAsset(name string, data []byte) error {
  367. tx, err := s.db.Begin()
  368. if err != nil {
  369. return err
  370. }
  371. stmt,_ := tx.Prepare("INSERT INTO assets(name, data) VALUES (?,?)")
  372. _, err = stmt.Exec(name, data)
  373. if err != nil {
  374. tx.Rollback()
  375. return err
  376. }
  377. tx.Commit()
  378. return nil
  379. }
  380. func (s *SQLiteRepo) AddDocument(doc Document) error {
  381. tx, err := s.db.Begin()
  382. if err != nil {
  383. return err
  384. }
  385. stmt,_ := tx.Prepare("INSERT INTO posts (id, title, created, body, category,sample) VALUES (?,?,?,?,?,?)")
  386. _, err = stmt.Exec(doc.Ident, doc.Title, doc.Created, doc.Body, doc.Category, doc.Sample)
  387. if err != nil {
  388. tx.Rollback()
  389. return err
  390. }
  391. tx.Commit()
  392. return nil
  393. }
  394. /*
  395. Delete a document from the db
  396. :param id: the identifier of the document to remove
  397. */
  398. func (s *SQLiteRepo) DeleteDocument(id Identifier) error {
  399. tx, err := s.db.Begin()
  400. if err != nil {
  401. return err
  402. }
  403. stmt,_ := tx.Prepare("DELETE FROM posts WHERE id=?")
  404. _, err = stmt.Exec(id)
  405. if err != nil {
  406. tx.Rollback()
  407. return err}
  408. tx.Commit()
  409. return nil
  410. }
  411. // Get all Hosts from the host table
  412. func (s *SQLiteRepo) AllDocuments() []Document {
  413. rows, err := s.db.Query("SELECT * FROM posts")
  414. if err != nil {
  415. fmt.Printf("There was an issue getting all posts. %s", err.Error())
  416. return nil
  417. }
  418. defer rows.Close()
  419. var all []Document
  420. for rows.Next() {
  421. var post Document
  422. if err := rows.Scan(&post.Ident, &post.Title, &post.Created, &post.Body, &post.Sample); err != nil {
  423. fmt.Printf("There was an error getting all documents. %s", err.Error())
  424. return nil
  425. }
  426. all = append(all, post)
  427. }
  428. return all
  429. }
  430. type InvalidSkipArg struct{ Skip int }
  431. func (i *InvalidSkipArg) Error() string {
  432. return fmt.Sprintf("Invalid skip amount was passed: %v", i.Skip)
  433. }
  434. type ImageStoreItem struct {
  435. Identifier string `json:"identifier"`
  436. Filename string `json:"filename"`
  437. AbsolutePath string `json:"absolute_path"`
  438. Title string `json:"title" form:"title"`
  439. Created string `json:"created"`
  440. Desc string `json:"description" form:"description"`
  441. Category string `json:"category"`
  442. ApiPath string
  443. }
  444. /*
  445. Create a new ImageStoreItem
  446. :param fname: the name of the file to be saved
  447. :param title: the canonical title to give the image
  448. :param desc: the description to associate to the image
  449. */
  450. func NewImageStoreItem(title string, desc string) Image {
  451. id := uuid.New()
  452. img := Image{
  453. Ident: id.String(),
  454. Title: title,
  455. Category: DIGITAL_ART,
  456. Location: GetImageStore(),
  457. Created: time.Now().UTC().String(),
  458. Desc: desc,
  459. }
  460. return img
  461. }
  462. /*
  463. Function to return the location of the image store. Wrapping the env call in
  464. a function so that refactoring is easier
  465. */
  466. func GetImageStore() string {
  467. return os.Getenv(env.IMAGE_STORE)
  468. }
  469. /*
  470. Return database entries of the images that exist in the imagestore
  471. :param rds: pointer to a RedisCaller to perform the lookups with
  472. */
  473. func GetImageData(rds *RedisCaller) ([]*ImageStoreItem, error) {
  474. ids, err := rds.GetByCategory(DIGITAL_ART)
  475. if err != nil {
  476. return nil, err
  477. }
  478. var imageEntries []*ImageStoreItem
  479. for i := range ids {
  480. val, err := rds.Client.Get(rds.ctx, ids[i]).Result()
  481. if err == redis.Nil {
  482. return nil, err
  483. } else if err != nil {
  484. return nil, err
  485. }
  486. data := []byte(val)
  487. var imageEntry ImageStoreItem
  488. err = json.Unmarshal(data, &imageEntry)
  489. if err != nil {
  490. return nil, err
  491. }
  492. imageEntry.ApiPath = fmt.Sprintf("/api/v1/images/%s", imageEntry.Filename)
  493. imageEntries = append(imageEntries, &imageEntry)
  494. }
  495. return imageEntries, err
  496. }