github.com/readium/readium-lcp-server@v0.0.0-20240509124024-799e77a0bbd6/frontend/webpublication/webpublication.go (about) 1 // Copyright 2020 Readium Foundation. All rights reserved. 2 // Use of this source code is governed by a BSD-style license 3 // that can be found in the LICENSE file exposed on Github (readium) in the project repository. 4 5 package webpublication 6 7 import ( 8 "database/sql" 9 "errors" 10 "io" 11 "io/ioutil" 12 "log" 13 "mime/multipart" 14 "os" 15 "path/filepath" 16 17 "github.com/readium/readium-lcp-server/config" 18 "github.com/readium/readium-lcp-server/dbutils" 19 "github.com/readium/readium-lcp-server/encrypt" 20 ) 21 22 // Publication status 23 const ( 24 StatusDraft string = "draft" 25 StatusEncrypting string = "encrypting" 26 StatusError string = "error" 27 StatusOk string = "ok" 28 ) 29 30 // ErrNotFound error trown when publication is not found 31 var ErrNotFound = errors.New("Publication not found") 32 33 // WebPublication interface for publication db interaction 34 type WebPublication interface { 35 Get(id int64) (Publication, error) 36 GetByUUID(uuid string) (Publication, error) 37 Add(publication Publication) error 38 Update(publication Publication) error 39 Delete(id int64) error 40 List(page int, pageNum int) func() (Publication, error) 41 Upload(multipart.File, string, *Publication) error 42 CheckByTitle(title string) (int64, error) 43 } 44 45 // Publication struct defines a publication 46 type Publication struct { 47 ID int64 `json:"id"` 48 UUID string `json:"uuid"` 49 Status string `json:"status"` 50 Title string `json:"title,omitempty"` 51 MasterFilename string `json:"masterFilename,omitempty"` 52 } 53 54 // PublicationManager helper 55 type PublicationManager struct { 56 db *sql.DB 57 dbGetByID *sql.Stmt 58 dbGetByUUID *sql.Stmt 59 dbCheckByTitle *sql.Stmt 60 dbGetMasterFile *sql.Stmt 61 dbList *sql.Stmt 62 } 63 64 // Get gets a publication by its ID 65 func (pubManager PublicationManager) Get(id int64) (Publication, error) { 66 67 row := pubManager.dbGetByID.QueryRow(id) 68 var pub Publication 69 err := row.Scan( 70 &pub.ID, 71 &pub.UUID, 72 &pub.Title, 73 &pub.Status) 74 return pub, err 75 } 76 77 // GetByUUID returns a publication by its uuid 78 func (pubManager PublicationManager) GetByUUID(uuid string) (Publication, error) { 79 80 row := pubManager.dbGetByUUID.QueryRow(uuid) 81 var pub Publication 82 err := row.Scan( 83 &pub.ID, 84 &pub.UUID, 85 &pub.Title, 86 &pub.Status) 87 return pub, err 88 } 89 90 // CheckByTitle checks if the title of a publication exists or not in the db 91 func (pubManager PublicationManager) CheckByTitle(title string) (int64, error) { 92 93 row := pubManager.dbCheckByTitle.QueryRow(title) 94 var res int64 95 err := row.Scan(&res) 96 if err != nil { 97 return -1, ErrNotFound 98 } 99 // returns 1 or 0 100 return res, err 101 } 102 103 // encryptPublication encrypts a publication, notifies the License Server 104 // and inserts a record in the database. 105 func encryptPublication(inputPath string, pub *Publication, pubManager PublicationManager) error { 106 107 // encrypt the publication 108 // FIXME: work on a direct storage of the output file. 109 outputRepo := config.Config.FrontendServer.EncryptedRepository 110 empty := "" 111 notification, err := encrypt.ProcessEncryption(empty, empty, inputPath, empty, outputRepo, empty, empty, empty, false) 112 if err != nil { 113 return err 114 } 115 116 // send a notification to the License Server v1 117 err = encrypt.NotifyLCPServer( 118 *notification, 119 config.Config.LcpServer.PublicBaseUrl, 120 false, 121 config.Config.LcpUpdateAuth.Username, 122 config.Config.LcpUpdateAuth.Password, 123 false) // non verbose 124 if err != nil { 125 return err 126 } 127 128 // store the new publication in the db 129 // the publication uuid is the lcp db content id. 130 pub.UUID = notification.UUID 131 pub.Status = StatusOk 132 _, err = pubManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, 133 "INSERT INTO publication (uuid, title, status) VALUES ( ?, ?, ?)"), 134 pub.UUID, pub.Title, pub.Status) 135 136 return err 137 } 138 139 // Add adds a new publication 140 // Encrypts a master File and notifies the License server 141 func (pubManager PublicationManager) Add(pub Publication) error { 142 143 // get the path to the master file 144 inputPath := filepath.Join( 145 config.Config.FrontendServer.MasterRepository, pub.MasterFilename) 146 147 if _, err := os.Stat(inputPath); err != nil { 148 // the master file does not exist 149 return err 150 } 151 152 // encrypt the publication and send a notification to the License server 153 err := encryptPublication(inputPath, &pub, pubManager) 154 if err != nil { 155 return err 156 } 157 158 // delete the master file 159 err = os.Remove(inputPath) 160 return err 161 } 162 163 // Upload creates a new publication, named after a POST form parameter. 164 // Encrypts a master File and notifies the License server 165 func (pubManager PublicationManager) Upload(file multipart.File, extension string, pub *Publication) error { 166 167 // create a temp file in the default directory 168 tmpfile, err := ioutil.TempFile("", "uploaded-*"+extension) 169 if err != nil { 170 return err 171 } 172 defer os.Remove(tmpfile.Name()) 173 174 // copy the request payload to the temp file 175 if _, err = io.Copy(tmpfile, file); err != nil { 176 return err 177 } 178 179 // close the temp file 180 if err = tmpfile.Close(); err != nil { 181 return err 182 } 183 184 // encrypt the publication and send a notification to the License server 185 return encryptPublication(tmpfile.Name(), pub, pubManager) 186 } 187 188 // Update updates a publication 189 // Only the title is updated 190 func (pubManager PublicationManager) Update(pub Publication) error { 191 192 _, err := pubManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, 193 "UPDATE publication SET title=?, status=? WHERE id = ?"), 194 pub.Title, pub.Status, pub.ID) 195 return err 196 } 197 198 // Delete deletes a publication, selected by its numeric id 199 func (pubManager PublicationManager) Delete(id int64) error { 200 201 var title string 202 row := pubManager.dbGetMasterFile.QueryRow(id) 203 err := row.Scan(&title) 204 if err != nil { 205 return err 206 } 207 208 // delete all purchases relative to this publication 209 _, err = pubManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, `DELETE FROM purchase WHERE publication_id=?`), id) 210 if err != nil { 211 return err 212 } 213 214 // delete the publication 215 _, err = pubManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "DELETE FROM publication WHERE id = ?"), id) 216 return err 217 } 218 219 // List lists publications within a given range 220 // Parameters: page = number of items per page; pageNum = page offset (0 for the first page) 221 func (pubManager PublicationManager) List(page, pageNum int) func() (Publication, error) { 222 223 var rows *sql.Rows 224 var err error 225 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 226 if driver == "mssql" { 227 rows, err = pubManager.dbList.Query(pageNum*page, page) 228 } else { 229 rows, err = pubManager.dbList.Query(page, pageNum*page) 230 } 231 if err != nil { 232 return func() (Publication, error) { return Publication{}, err } 233 } 234 235 return func() (Publication, error) { 236 var pub Publication 237 var err error 238 if rows.Next() { 239 err = rows.Scan(&pub.ID, &pub.UUID, &pub.Title, &pub.Status) 240 } else { 241 rows.Close() 242 err = ErrNotFound 243 } 244 return pub, err 245 } 246 } 247 248 // Init initializes the publication manager 249 // Creates the publication db table. 250 func Init(db *sql.DB) (i WebPublication, err error) { 251 252 driver, _ := config.GetDatabase(config.Config.FrontendServer.Database) 253 254 // if sqlite, create the content table in the frontend db if it does not exist 255 if driver == "sqlite3" { 256 _, err = db.Exec(tableDef) 257 if err != nil { 258 log.Println("Error creating publication table") 259 return 260 } 261 } 262 263 var dbGetByID *sql.Stmt 264 dbGetByID, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "SELECT id, uuid, title, status FROM publication WHERE id = ?")) 265 if err != nil { 266 return 267 } 268 269 var dbGetByUUID *sql.Stmt 270 dbGetByUUID, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "SELECT id, uuid, title, status FROM publication WHERE uuid = ?")) 271 if err != nil { 272 return 273 } 274 275 var dbCheckByTitle *sql.Stmt 276 dbCheckByTitle, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "SELECT COUNT(1) FROM publication WHERE title = ?")) 277 if err != nil { 278 return 279 } 280 281 var dbGetMasterFile *sql.Stmt 282 dbGetMasterFile, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "SELECT title FROM publication WHERE id = ?")) 283 if err != nil { 284 return 285 } 286 287 var dbList *sql.Stmt 288 if driver == "mssql" { 289 dbList, err = db.Prepare("SELECT id, uuid, title, status FROM publication ORDER BY id desc OFFSET ? ROWS FETCH NEXT ? ROWS ONLY") 290 } else { 291 dbList, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "SELECT id, uuid, title, status FROM publication ORDER BY id desc LIMIT ? OFFSET ?")) 292 293 } 294 if err != nil { 295 return 296 } 297 298 i = PublicationManager{db, dbGetByID, dbGetByUUID, dbCheckByTitle, dbGetMasterFile, dbList} 299 return 300 } 301 302 const tableDef = "CREATE TABLE IF NOT EXISTS publication (" + 303 "id integer NOT NULL PRIMARY KEY," + 304 "uuid varchar(255) NOT NULL," + 305 "title varchar(255) NOT NULL," + 306 "status varchar(255) NOT NULL" + 307 ");" + 308 "CREATE INDEX IF NOT EXISTS uuid_index ON publication (uuid);"