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);"