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