github.com/readium/readium-lcp-server@v0.0.0-20240101192032-6e95190e99f1/lcpserver/api/store.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 apilcp 6 7 import ( 8 "crypto/tls" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "io/ioutil" 14 "net/http" 15 "net/url" 16 "os" 17 "strconv" 18 19 "github.com/gorilla/mux" 20 21 "github.com/readium/readium-lcp-server/api" 22 "github.com/readium/readium-lcp-server/index" 23 "github.com/readium/readium-lcp-server/license" 24 "github.com/readium/readium-lcp-server/logging" 25 "github.com/readium/readium-lcp-server/pack" 26 "github.com/readium/readium-lcp-server/problem" 27 "github.com/readium/readium-lcp-server/storage" 28 ) 29 30 // Server groups functions used by the lcp server 31 type Server interface { 32 Store() storage.Store 33 Index() index.Index 34 Licenses() license.Store 35 Certificate() *tls.Certificate 36 Source() *pack.ManualSource 37 } 38 39 // Encrypted is used for communication with the License Server 40 type Encrypted struct { 41 ContentID string `json:"content-id"` 42 ContentKey []byte `json:"content-encryption-key"` 43 StorageMode int `json:"storage-mode"` 44 Output string `json:"protected-content-location"` 45 FileName string `json:"protected-content-disposition"` 46 Size int64 `json:"protected-content-length"` 47 Checksum string `json:"protected-content-sha256"` 48 ContentType string `json:"protected-content-type,omitempty"` 49 } 50 51 const ( 52 Storage_none = 0 53 Storage_s3 = 1 54 Storage_fs = 2 55 ) 56 57 func writeRequestFileToTemp(r io.Reader) (int64, *os.File, error) { 58 dir := os.TempDir() 59 file, err := ioutil.TempFile(dir, "readium-lcp") 60 if err != nil { 61 return 0, file, err 62 } 63 64 n, err := io.Copy(file, r) 65 66 // Rewind to the beginning of the file 67 file.Seek(0, 0) 68 69 return n, file, err 70 } 71 72 func cleanupTempFile(f *os.File) { 73 if f == nil { 74 return 75 } 76 f.Close() 77 os.Remove(f.Name()) 78 } 79 80 // StoreContent stores content passed through the request body into the storage. 81 // The content name is given in the url (name) 82 // A temporary file is created, then deleted after the content has been stored. 83 // This function is using an async task. 84 func StoreContent(w http.ResponseWriter, r *http.Request, s Server) { 85 86 vars := mux.Vars(r) 87 88 size, f, err := writeRequestFileToTemp(r.Body) 89 if err != nil { 90 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 91 return 92 } 93 94 defer cleanupTempFile(f) 95 96 t := pack.NewTask(vars["name"], f, size) 97 result := s.Source().Post(t) 98 99 if result.Error != nil { 100 problem.Error(w, r, problem.Problem{Detail: result.Error.Error()}, http.StatusBadRequest) 101 return 102 } 103 104 // must come *after* w.Header().Add()/Set(), but before w.Write() 105 w.WriteHeader(http.StatusCreated) 106 107 json.NewEncoder(w).Encode(result.ID) 108 } 109 110 // AddContent adds content to the storage 111 // lcp spec : store data resulting from an external encryption 112 // PUT method with PAYLOAD : encrypted publication in json format 113 // This method adds an encrypted file to a store 114 // and adds the corresponding decryption key to the database. 115 // The content_id is taken from the url. 116 // The input file is then deleted. 117 func AddContent(w http.ResponseWriter, r *http.Request, s Server) { 118 119 // parse the json payload 120 vars := mux.Vars(r) 121 decoder := json.NewDecoder(r.Body) 122 var encrypted Encrypted 123 err := decoder.Decode(&encrypted) 124 if err != nil { 125 http.Error(w, err.Error(), http.StatusBadRequest) 126 } 127 // get the content ID in the url 128 contentID := vars["content_id"] 129 if contentID == "" { 130 problem.Error(w, r, problem.Problem{Detail: "The content id must be set in the url"}, http.StatusBadRequest) 131 return 132 } 133 134 // add a log 135 logging.Print("Add publication " + contentID) 136 137 // if the encrypted publication has not been already stored by lcpencrypt 138 if encrypted.StorageMode == Storage_none { 139 140 // open the encrypted file, use its full path 141 file, err := getAndOpenFile(encrypted.Output) 142 if err != nil { 143 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 144 return 145 } 146 // the input file will be deleted when the function returns 147 defer cleanupTempFile(file) 148 149 // add the file to the storage, named by contentID, without file extension 150 _, err = s.Store().Add(contentID, file) 151 if err != nil { 152 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 153 return 154 } 155 } 156 157 // insert a row in the database if the content id does not already exist 158 // or update the database with a new content key and file location if the content id already exists 159 var c index.Content 160 c, err = s.Index().Get(contentID) 161 c.EncryptionKey = encrypted.ContentKey 162 // the Location field contains either the file name (useful during download) 163 // or the storage URL of the encrypted, depending the storage mode. 164 if encrypted.StorageMode != Storage_none { 165 c.Location = encrypted.Output 166 } else { 167 c.Location = encrypted.FileName 168 } 169 c.Length = encrypted.Size 170 c.Sha256 = encrypted.Checksum 171 c.Type = encrypted.ContentType 172 173 code := http.StatusCreated 174 if err == index.ErrNotFound { //insert into database 175 c.ID = contentID 176 err = s.Index().Add(c) 177 } else { //update the encryption key for c.ID = encrypted.ContentID 178 err = s.Index().Update(c) 179 code = http.StatusOK 180 } 181 if err != nil { //if db not updated 182 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusInternalServerError) 183 return 184 } 185 186 // set the response http code 187 w.WriteHeader(code) 188 } 189 190 // ListContents lists the content in the storage index 191 func ListContents(w http.ResponseWriter, r *http.Request, s Server) { 192 193 fn := s.Index().List() 194 contents := make([]index.Content, 0) 195 196 for it, err := fn(); err == nil; it, err = fn() { 197 contents = append(contents, it) 198 } 199 200 // add a log 201 logging.Print("List publications, total " + strconv.Itoa(len(contents))) 202 203 w.Header().Set("Content-Type", api.ContentType_JSON) 204 enc := json.NewEncoder(w) 205 err := enc.Encode(contents) 206 if err != nil { 207 problem.Error(w, r, problem.Problem{Detail: err.Error()}, http.StatusBadRequest) 208 return 209 } 210 211 } 212 213 // GetContent fetches and returns an encrypted content file 214 // selected by it content id (uuid) 215 func GetContent(w http.ResponseWriter, r *http.Request, s Server) { 216 217 // get the content id from the calling url 218 vars := mux.Vars(r) 219 contentID := vars["content_id"] 220 221 // add a log 222 logging.Print("Fetch content " + contentID) 223 224 content, err := s.Index().Get(contentID) 225 if err != nil { //item probably not found 226 if err == index.ErrNotFound { 227 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusNotFound) 228 } else { 229 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 230 } 231 return 232 } 233 234 // check the existence of the file 235 item, err := s.Store().Get(contentID) 236 if err != nil { //item probably not found 237 if err == storage.ErrNotFound { 238 problem.Error(w, r, problem.Problem{Detail: "Storage:" + err.Error(), Instance: contentID}, http.StatusNotFound) 239 } else { 240 problem.Error(w, r, problem.Problem{Detail: "Storage:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 241 } 242 return 243 } 244 // opens the file 245 contentReadCloser, err := item.Contents() 246 if err != nil { 247 problem.Error(w, r, problem.Problem{Detail: "File:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 248 return 249 } 250 251 defer contentReadCloser.Close() 252 if err != nil { //file probably not found 253 problem.Error(w, r, problem.Problem{Detail: err.Error(), Instance: contentID}, http.StatusBadRequest) 254 return 255 } 256 // set headers 257 w.Header().Set("Content-Disposition", "attachment; filename="+content.Location) 258 w.Header().Set("Content-Type", content.Type) 259 w.Header().Set("Content-Length", fmt.Sprintf("%d", content.Length)) 260 261 // returns the content of the file to the caller 262 io.Copy(w, contentReadCloser) 263 } 264 265 // DeleteContent deletes a record 266 func DeleteContent(w http.ResponseWriter, r *http.Request, s Server) { 267 268 // get the content id from the calling url 269 vars := mux.Vars(r) 270 contentID := vars["content_id"] 271 272 // add a log 273 logging.Print("Delete publication " + contentID) 274 275 err := s.Index().Delete(contentID) 276 if err != nil { //item probably not found 277 if err == index.ErrNotFound { 278 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusNotFound) 279 } else { 280 problem.Error(w, r, problem.Problem{Detail: "Index:" + err.Error(), Instance: contentID}, http.StatusInternalServerError) 281 } 282 return 283 } 284 // set the response http code 285 w.WriteHeader(http.StatusOK) 286 287 } 288 289 // getAndOpenFile opens a file from a path, or downloads then opens it if its location is a URL 290 func getAndOpenFile(filePathOrURL string) (*os.File, error) { 291 292 isURL, err := isURL(filePathOrURL) 293 if err != nil { 294 return nil, err 295 } 296 297 if isURL { 298 return downloadAndOpenFile(filePathOrURL) 299 } 300 301 return os.Open(filePathOrURL) 302 } 303 304 func downloadAndOpenFile(url string) (*os.File, error) { 305 file, _ := ioutil.TempFile("", "") 306 fileName := file.Name() 307 308 err := downloadFile(url, fileName) 309 310 if err != nil { 311 return nil, err 312 } 313 314 return os.Open(fileName) 315 } 316 317 func isURL(filePathOrURL string) (bool, error) { 318 url, err := url.Parse(filePathOrURL) 319 if err != nil { 320 return false, errors.New("error parsing input string") 321 } 322 return url.Scheme == "http" || url.Scheme == "https", nil 323 } 324 325 func downloadFile(url string, targetFilePath string) error { 326 out, err := os.Create(targetFilePath) 327 if err != nil { 328 return err 329 } 330 defer out.Close() 331 332 resp, err := http.Get(url) 333 if err != nil { 334 return err 335 } 336 337 if resp.StatusCode >= 300 { 338 return fmt.Errorf("HTTP response: %d %s when downloading %s", resp.StatusCode, resp.Status, url) 339 } 340 341 defer resp.Body.Close() 342 343 _, err = io.Copy(out, resp.Body) 344 if err != nil { 345 return err 346 } 347 348 return nil 349 }