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  }