storage.go 14 KB

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