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