Also implement Previous/Next buttons.
Series: <input id="ser" type="text"/>
</form>
- <h1>Books</h1>
-
- <div id="books">(No books found)</div>
-
<div class="footer">
- <input id="back" value="Back" type="button"/>
- <input id="forward" value="Forward" type="button"/>
+ <input id="back" onclick="onPrev();" value="Back" type="button"/>
+ <input id="forward" onclick="onNext();" value="Forward" type="button"/>
Showing <span id="first">0</span> through <span id="last">0</span> out of <span id="count">0</span> matching books.
</div>
+ <div id="books">(No books found)</div>
+
<script src="lib.js"></script>
</body>
</html>
count: 0,
first: 0,
ids: [],
- last: 0,
+ last: (-1),
+ pageSize: 100
}
function constructSearchUrl() {
}
function onNext() {
+ if (g_state.last < (g_state.count - 1)) {
+ g_state.first += g_state.pageSize;
+ g_state.last += g_state.pageSize;
+ if (g_state.last >= g_state.count) {
+ g_state.last = g_state.count - 1;
+ }
+
+ refreshData();
+ }
}
function onPrev() {
+ if (g_state.first > 0) {
+ g_state.first -= g_state.pageSize;
+ if (g_state.first < 0) {
+ g_state.first = 0;
+ }
+ g_state.last = g_state.first + g_state.pageSize;
+
+ refreshData();
+ }
}
function onSearch() {
.then((jsonValue) => {
console.log('JSON response: ', jsonValue);
g_state.ids = jsonValue
+ g_state.count = g_state.ids.length;
g_state.first = 0
g_state.last = (g_state.ids.length) - 1;
- if (g_state.last > 100) {
- g_state.last = 100;
+ if (g_state.last > g_state.pageSize) {
+ g_state.last = g_state.pageSize;
}
refreshData()
})
var i;
var url = '/info/?ids=';
for (i = g_state.first; i <= g_state.last; ++i) {
- if (i > 0) {
+ if (i > g_state.first) {
url += ',';
}
url += g_state.ids[i];
function refreshLayout() {
var i;
var html = '';
- for (i = g_state.first; i <= g_state.last; ++i) {
+ var limit = g_state.last - g_state.first;
+ for (i = 0; i <= limit; ++i) {
var book = g_state.cache[i];
html += bookHtml(book);
}
document.getElementById('books').innerHTML = html;
+ document.getElementById('first').innerHTML = (g_state.first + 1);
+ document.getElementById('last').innerHTML = (g_state.last + 1);
+ document.getElementById('count').innerHTML = g_state.count;
}
function report(message) {
function bookHtml(book) {
console.log('bookHtml(): ', book);
- var result = '<div class="book"><table><tr><td><a href="/book/' + book.Id + '">'
- + '<img class="cover-thumb" src="/download/' + book.CoverId + '"/></a></td>'
- + '<td><span class="popup">' + book.Description + '</span></td>'
- + '</tr></table></div>';
+ var result = '<div class="book">'
+ + '<table>'
+ + '<tr>'
+ + '<td><a href="/book/' + book.Id + '">';
+ if (0 == book.CoverId) {
+ result += '(No cover available)'
+ } else {
+ result += '<img class="cover-thumb" src="/download/' + book.CoverId + '"/>'
+ }
+ result += '</a></td>'
+ + '<td>'
+ + '<p><b>' + book.Title + '</b></p>'
+ + '<p>'
+ + '<i>' + book.AuthorReading + '</i>';
+ if (typeof(book.SeriesName) !== 'undefined' && book.SeriesName.length > 0) {
+ result += '<br/><i>' + book.SeriesName + ' ' + book.Volume + '</i>';
+ }
+ result += '</p>'
+ + '</td>'
+ + '</tr>'
+ + '</table>'
+ + '</div>';
return result;
}
return res
}
+func queryBookPathById(id int) (string) {
+ query := "SELECT b.path FROM Books b WHERE b.id=$1"
+
+ ps, err := getDb().Prepare(query)
+ if nil != err {
+ report("Failed to Prepare query: " + query, err)
+ return ""
+ }
+ defer ps.Close()
+
+ row := ps.QueryRow(id)
+ var path sql.NullString
+ err = row.Scan(&path)
+ if nil != err {
+ report(fmt.Sprintf("Failed to retrieve path for book id %v: ", id), err)
+ return ""
+ }
+
+ return nsVal(path)
+}
+
func queryIds(criteria []SearchTerm) []int {
fmt.Println("queryIds():", criteria)
query := "SELECT b.id FROM Books b" +
" INNER JOIN Authors a ON a.id=b.author" +
- " LEFT OUTER JOIN Series s ON s.id=b.series"
+ " LEFT OUTER JOIN Series s ON s.id=b.series"
args := make([]interface{}, len(criteria))
args[i] = criterion.Text
}
+ query += " ORDER BY b.path"
+
res := []int{}
ps, err := getDb().Prepare(query)
}
defer rows.Close()
- for rows.Next(); rows.Next(); {
+ for rows.Next() {
var id int
rows.Scan(&id)
res = append(res, id)
"io"
"net/http"
"os"
+ "path/filepath"
"strconv"
"strings"
)
const MAX_TERMS = 10
// ============================================================================
+func bookMimeType(bookPath string) string {
+ upper := strings.ToUpper(bookPath)
+
+ if strings.HasSuffix(upper, ".EPUB") {
+ return "application/epub+zip"
+ } else if strings.HasSuffix(upper, ".PDF") {
+ return "application/pdf"
+ } else {
+ fmt.Println("Warning: Cannot determine MIME type, will use application/octet-stream:", bookPath)
+ return "application/octet-stream"
+ }
+}
+
func efsPathForId(efsId int) string {
config := getConfig()
action := strings.Split(r.URL.Path[1:], "/")[0]
switch(action) {
+ case "book":
+ handleBook(w, r)
case "download":
handleDownload(w, r)
case "info":
}
}
-/*
-func handleApp(w http.ResponseWriter, r *http.Request) {
- fmt.Println("handleApp():", r.URL.Path)
+// Download a book, based on the path stored in the DB
+func handleBook(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path[1:]
+ tok := strings.Split(path, "/")
+ if 2 != len(tok) {
+ fmt.Fprintln(w, "Unexpected path for book download:", path)
+ return
+ }
+
+ bookId, err := strconv.Atoi(tok[1])
+ if (nil != err) || (0 == bookId) {
+ fmt.Fprintln(w, "Invalid id for book download:", path, err)
+ return
+ }
+
+ bookPath := queryBookPathById(bookId)
+ if 0 == len(bookPath) {
+ fmt.Fprintln(w, "No book for ID:", bookId)
+ return
+ }
+
+ bookFileName := filepath.Base(bookPath)
- // Security check: prevent walking up the directory
- pos := strings.Index(r.Url.Path, "../")
- if (-1) == pos {
- fmt.Fprintln(w, "Paths containing \"../\" are not permitted:", r.URL.Path)
+ mimeType := bookMimeType(bookPath)
+ if 0 == len(mimeType) {
+ fmt.Fprintln(w, "Failed to determine MIME type for book:", bookPath)
return
}
- fileName := "../app" + r.URL.Path
- _, err := os.Stat(fileName)
- if nil != err {
- fmt.Fprintln(w, "Failed to find file:", fileName, err)
+ var info os.FileInfo
+ info, err = os.Stat(bookPath)
+ if nil != err {
+ fmt.Fprintln(w, "Failed to read file metadata:", bookPath, err)
return
}
+ modTime := info.ModTime()
+
+ var fd *os.File
+ fd, err = os.Open(bookPath)
+ if nil != err {
+ fmt.Fprintln(w, "Failed to open file:", bookPath, err)
+ return
+ }
+ defer fd.Close()
- http.ServeFile(w, r, "../app/" + r.URL.Path[1:])
+ // TODO: handle non-ASCII file names. Need to look up the permutations on how to encode that.
+ w.Header().Set("Content-Disposition", "attachment; filename=" + bookFileName)
+ w.Header().Set("Content-Type", mimeType)
+ http.ServeContent(w, r, bookFileName, modTime, fd)
}
-*/
func handleDownload(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path[1:]
}
defer fd.Close()
+ w.Header().Set("Content-Type", mimeType)
io.Copy(w, fd)
}