new app: scanning cabinet
WARNING: this app is still experimental, and even its data schema might
change. Do not use in production.
This change adds a Camlistore-based port of the scanning cabinet app
originally created by Brad Fitzpatrick:
https://github.com/bradfitz/scanningcabinet
Some of it is inspired from the App Engine Go port of Patrick Borgeest:
https://bitbucket.org/pborgeest/nometicland
The data schema is roughly as follows:
-a scan is a permanode, with the node type: "scanningcabinet:scan".
-a scan's camliContent attribute is set to the actual image file.
-a scan also holds the "dateCreated" attribute, as well as the
"document" attribute, which references the document this scan is a part
of (if any).
-a document is a permanode, with the node type: "scanningcabinet:doc".
-a document page, is modeled by the "camliPath:sha1-xxx" = "pageNumber"
relation, where sha1-xxx is the blobRef of a scan.
-a document can also hold the following attributes: "dateCreated",
"tag", "locationText", "title", "startDate", and "paymentDueDate".
Known caveats, in decreasing order of concern:
-the data schema might still change.
-the scancab tool, to actually create and upload the files from physical
documents, is practically untested (since I do not own a scanner).
-some parts, in particular related to searches, are probably
sub-optimized.
-the usual unavoidable bugs.
Change-Id: If6afc509e13f7c21164a3abd276fec075a3813bb
2016-07-07 15:53:57 +00:00
/ *
Copyright 2017 The Camlistore Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package main
import (
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"
"regexp"
"strconv"
"strings"
"time"
2017-03-19 05:27:00 +00:00
uistatic "camlistore.org/app/scanningcabinet/ui"
new app: scanning cabinet
WARNING: this app is still experimental, and even its data schema might
change. Do not use in production.
This change adds a Camlistore-based port of the scanning cabinet app
originally created by Brad Fitzpatrick:
https://github.com/bradfitz/scanningcabinet
Some of it is inspired from the App Engine Go port of Patrick Borgeest:
https://bitbucket.org/pborgeest/nometicland
The data schema is roughly as follows:
-a scan is a permanode, with the node type: "scanningcabinet:scan".
-a scan's camliContent attribute is set to the actual image file.
-a scan also holds the "dateCreated" attribute, as well as the
"document" attribute, which references the document this scan is a part
of (if any).
-a document is a permanode, with the node type: "scanningcabinet:doc".
-a document page, is modeled by the "camliPath:sha1-xxx" = "pageNumber"
relation, where sha1-xxx is the blobRef of a scan.
-a document can also hold the following attributes: "dateCreated",
"tag", "locationText", "title", "startDate", and "paymentDueDate".
Known caveats, in decreasing order of concern:
-the data schema might still change.
-the scancab tool, to actually create and upload the files from physical
documents, is practically untested (since I do not own a scanner).
-some parts, in particular related to searches, are probably
sub-optimized.
-the usual unavoidable bugs.
Change-Id: If6afc509e13f7c21164a3abd276fec075a3813bb
2016-07-07 15:53:57 +00:00
"camlistore.org/pkg/app"
"camlistore.org/pkg/auth"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/client"
"camlistore.org/pkg/constants"
2017-03-19 05:27:00 +00:00
"camlistore.org/pkg/fileembed"
new app: scanning cabinet
WARNING: this app is still experimental, and even its data schema might
change. Do not use in production.
This change adds a Camlistore-based port of the scanning cabinet app
originally created by Brad Fitzpatrick:
https://github.com/bradfitz/scanningcabinet
Some of it is inspired from the App Engine Go port of Patrick Borgeest:
https://bitbucket.org/pborgeest/nometicland
The data schema is roughly as follows:
-a scan is a permanode, with the node type: "scanningcabinet:scan".
-a scan's camliContent attribute is set to the actual image file.
-a scan also holds the "dateCreated" attribute, as well as the
"document" attribute, which references the document this scan is a part
of (if any).
-a document is a permanode, with the node type: "scanningcabinet:doc".
-a document page, is modeled by the "camliPath:sha1-xxx" = "pageNumber"
relation, where sha1-xxx is the blobRef of a scan.
-a document can also hold the following attributes: "dateCreated",
"tag", "locationText", "title", "startDate", and "paymentDueDate".
Known caveats, in decreasing order of concern:
-the data schema might still change.
-the scancab tool, to actually create and upload the files from physical
documents, is practically untested (since I do not own a scanner).
-some parts, in particular related to searches, are probably
sub-optimized.
-the usual unavoidable bugs.
Change-Id: If6afc509e13f7c21164a3abd276fec075a3813bb
2016-07-07 15:53:57 +00:00
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/magic"
"camlistore.org/pkg/search"
camliserver "camlistore.org/pkg/server"
"go4.org/syncutil"
)
const (
maxScan = 50 // number of scans fetched/displayed. arbitrary.
maxDue = 30 // number of due documents fetched
scanNodeType = "scanningcabinet:scan"
documentNodeType = "scanningcabinet:doc"
)
var (
rootTemplate = template . Must ( template . New ( "root" ) . Parse ( rootHTML ) )
docTemplate = template . Must ( template . New ( "doc" ) . Parse ( docHTML ) )
resourcePattern * regexp . Regexp = regexp . MustCompile ( ` ^/resource/( ` + blob . Pattern + ` )$ ` )
)
// config is used to unmarshal the application configuration JSON
// that we get from Camlistore when we request it at $CAMLI_APP_CONFIG_URL.
type extraConfig struct {
2017-03-19 05:27:00 +00:00
Auth string ` json:"auth,omitempty" ` // userpass:username:password
HTTPSCert string ` json:"httpsCert,omitempty" ` // path to the HTTPS certificate file.
HTTPSKey string ` json:"httpsKey,omitempty" ` // path to the HTTPS key file.
SourceRoot string ` json:"sourceRoot,omitempty" ` // Path to the app's resources dir, such as html and css files.
new app: scanning cabinet
WARNING: this app is still experimental, and even its data schema might
change. Do not use in production.
This change adds a Camlistore-based port of the scanning cabinet app
originally created by Brad Fitzpatrick:
https://github.com/bradfitz/scanningcabinet
Some of it is inspired from the App Engine Go port of Patrick Borgeest:
https://bitbucket.org/pborgeest/nometicland
The data schema is roughly as follows:
-a scan is a permanode, with the node type: "scanningcabinet:scan".
-a scan's camliContent attribute is set to the actual image file.
-a scan also holds the "dateCreated" attribute, as well as the
"document" attribute, which references the document this scan is a part
of (if any).
-a document is a permanode, with the node type: "scanningcabinet:doc".
-a document page, is modeled by the "camliPath:sha1-xxx" = "pageNumber"
relation, where sha1-xxx is the blobRef of a scan.
-a document can also hold the following attributes: "dateCreated",
"tag", "locationText", "title", "startDate", and "paymentDueDate".
Known caveats, in decreasing order of concern:
-the data schema might still change.
-the scancab tool, to actually create and upload the files from physical
documents, is practically untested (since I do not own a scanner).
-some parts, in particular related to searches, are probably
sub-optimized.
-the usual unavoidable bugs.
Change-Id: If6afc509e13f7c21164a3abd276fec075a3813bb
2016-07-07 15:53:57 +00:00
}
func appConfig ( ) ( * extraConfig , error ) {
configURL := os . Getenv ( "CAMLI_APP_CONFIG_URL" )
if configURL == "" {
log . Printf ( "CAMLI_APP_CONFIG_URL not defined, the app will run without any auth" )
return nil , nil
}
cl , err := app . Client ( )
if err != nil {
return nil , fmt . Errorf ( "could not get a client to fetch extra config: %v" , err )
}
conf := & extraConfig { }
if err := cl . GetJSON ( configURL , conf ) ; err != nil {
return nil , fmt . Errorf ( "could not get app extra config at %v: %v" , configURL , err )
}
return conf , nil
}
type handler struct {
httpsCert string
httpsKey string
am auth . AuthMode
mux * http . ServeMux
sh search . QueryDescriber
// TODO(mpl): later we should have an uploader interface instead. implemented by *client.Client like sh, but they wouldn't have to be the same in theory. right now they actually are.
cl * client . Client
ih * camliserver . ImageHandler
signer blob . Ref
server string
}
func newHandler ( ) ( * handler , error ) {
cl , err := app . Client ( )
if err != nil {
return nil , fmt . Errorf ( "could not initialize a client: %v" , err )
}
h := & handler {
sh : cl ,
cl : cl ,
}
2017-03-19 05:27:00 +00:00
config , err := appConfig ( )
if err != nil {
return nil , err
}
// Serve files from source root when running devcam
if config . SourceRoot != "" {
log . Printf ( "Using UI resources (HTML, JS, CSS) from disk, under %v" , config . SourceRoot )
uistatic . Files = & fileembed . Files {
DirFallback : config . SourceRoot ,
}
}
new app: scanning cabinet
WARNING: this app is still experimental, and even its data schema might
change. Do not use in production.
This change adds a Camlistore-based port of the scanning cabinet app
originally created by Brad Fitzpatrick:
https://github.com/bradfitz/scanningcabinet
Some of it is inspired from the App Engine Go port of Patrick Borgeest:
https://bitbucket.org/pborgeest/nometicland
The data schema is roughly as follows:
-a scan is a permanode, with the node type: "scanningcabinet:scan".
-a scan's camliContent attribute is set to the actual image file.
-a scan also holds the "dateCreated" attribute, as well as the
"document" attribute, which references the document this scan is a part
of (if any).
-a document is a permanode, with the node type: "scanningcabinet:doc".
-a document page, is modeled by the "camliPath:sha1-xxx" = "pageNumber"
relation, where sha1-xxx is the blobRef of a scan.
-a document can also hold the following attributes: "dateCreated",
"tag", "locationText", "title", "startDate", and "paymentDueDate".
Known caveats, in decreasing order of concern:
-the data schema might still change.
-the scancab tool, to actually create and upload the files from physical
documents, is practically untested (since I do not own a scanner).
-some parts, in particular related to searches, are probably
sub-optimized.
-the usual unavoidable bugs.
Change-Id: If6afc509e13f7c21164a3abd276fec075a3813bb
2016-07-07 15:53:57 +00:00
mux := http . NewServeMux ( )
mux . HandleFunc ( "/" , h . handleRoot )
2017-03-19 05:27:00 +00:00
mux . HandleFunc ( "/ui/" , handleUiFile )
new app: scanning cabinet
WARNING: this app is still experimental, and even its data schema might
change. Do not use in production.
This change adds a Camlistore-based port of the scanning cabinet app
originally created by Brad Fitzpatrick:
https://github.com/bradfitz/scanningcabinet
Some of it is inspired from the App Engine Go port of Patrick Borgeest:
https://bitbucket.org/pborgeest/nometicland
The data schema is roughly as follows:
-a scan is a permanode, with the node type: "scanningcabinet:scan".
-a scan's camliContent attribute is set to the actual image file.
-a scan also holds the "dateCreated" attribute, as well as the
"document" attribute, which references the document this scan is a part
of (if any).
-a document is a permanode, with the node type: "scanningcabinet:doc".
-a document page, is modeled by the "camliPath:sha1-xxx" = "pageNumber"
relation, where sha1-xxx is the blobRef of a scan.
-a document can also hold the following attributes: "dateCreated",
"tag", "locationText", "title", "startDate", and "paymentDueDate".
Known caveats, in decreasing order of concern:
-the data schema might still change.
-the scancab tool, to actually create and upload the files from physical
documents, is practically untested (since I do not own a scanner).
-some parts, in particular related to searches, are probably
sub-optimized.
-the usual unavoidable bugs.
Change-Id: If6afc509e13f7c21164a3abd276fec075a3813bb
2016-07-07 15:53:57 +00:00
mux . HandleFunc ( "/uploadurl" , h . handleUploadURL )
mux . HandleFunc ( "/upload" , h . handleUpload )
mux . HandleFunc ( "/resource/" , h . handleResource )
mux . HandleFunc ( "/makedoc" , h . handleMakedoc )
mux . HandleFunc ( "/doc/" , h . handleDoc )
mux . HandleFunc ( "/changedoc" , h . handleChangedoc )
mux . HandleFunc ( "/robots.txt" , handleRobots )
h . mux = mux
if err := h . disco ( ) ; err != nil {
return nil , err
}
if config != nil {
h . httpsCert = config . HTTPSCert
h . httpsKey = config . HTTPSKey
}
var authConfig string
if config == nil || config . Auth == "" {
authConfig = "none"
} else {
authConfig = config . Auth
}
am , err := auth . FromConfig ( authConfig )
if err != nil {
return nil , err
}
h . am = am
return h , nil
}
func ( h * handler ) ServeHTTP ( w http . ResponseWriter , r * http . Request ) {
if auth . AllowedWithAuth ( h . am , r , auth . OpAll ) {
h . serveHTTP ( w , r )
return
}
if us , ok := h . am . ( auth . UnauthorizedSender ) ; ok {
if us . SendUnauthorized ( w , r ) {
return
}
}
w . Header ( ) . Set ( "WWW-Authenticate" , "Basic realm=scanning cabinet" )
w . WriteHeader ( http . StatusUnauthorized )
fmt . Fprintf ( w , "<html><body><h1>Unauthorized</h1>" )
}
func ( h * handler ) serveHTTP ( w http . ResponseWriter , r * http . Request ) {
if h . mux == nil {
http . Error ( w , "handler not properly initialized" , http . StatusInternalServerError )
return
}
r . URL . Path = strings . Replace ( r . URL . Path , app . PathPrefix ( r ) , "/" , 1 )
h . mux . ServeHTTP ( w , r )
}
type rootData struct {
BaseURL string
Tags string
Media [ ] MediaObjectVM
SearchedDocs [ ] DocumentVM
UntaggedDocs [ ] DocumentVM
UpcomingDocs [ ] DocumentVM
TopMessage template . HTML
ErrorMessage string
AllTags [ ] string
}
func ( h * handler ) disco ( ) error {
var err error
server := os . Getenv ( "CAMLI_API_HOST" )
if server == "" {
server , err = h . cl . BlobRoot ( )
if err != nil {
return fmt . Errorf ( "CAMLI_API_HOST var not set, and client could not discover server blob root: %v" , err )
}
}
h . server = server
// TODO(mpl): setup our own signer if we got our own key and stuff.
signer , err := h . cl . ServerPublicKeyBlobRef ( )
if err != nil {
return fmt . Errorf ( "client has no signing capability and server can't sign for us either: %v" , err )
}
h . signer = signer
return nil
}
func ( h * handler ) handleRoot ( w http . ResponseWriter , r * http . Request ) {
topMessage := ""
if saved_doc := r . FormValue ( "saved_doc" ) ; saved_doc != "" {
topMessage = fmt . Sprintf ( "Saved <a href='doc/%s'>doc %s</a>" , saved_doc , saved_doc )
}
errorMessage := r . FormValue ( "error_message" )
limit := maxScan
if limitparam := r . FormValue ( "limit" ) ; limitparam != "" {
newlimit , err := strconv . Atoi ( limitparam )
if err == nil {
limit = newlimit
}
}
var (
movm [ ] MediaObjectVM
searchedDocs [ ] DocumentVM
allTags [ ] string
)
tags := newSeparatedString ( r . FormValue ( "tags" ) )
docs , err := h . fetchDocuments ( limit , searchOpts { tags : tags } )
if err != nil {
httputil . ServeError ( w , r , err )
return
}
if len ( tags ) != 0 {
searchedDocs = MakeDocumentViewModels ( docs )
// We've just done a search, in which case we don't show the scans,
// so no need to look for them. Nor do we look for/show the tags cloud.
} else {
// fetch media objects
mediaObjects , err := h . fetchScans ( limit )
if err != nil {
httputil . ServeError ( w , r , err )
return
}
movm = MakeMediaObjectViewModels ( mediaObjects )
// TODO(mpl): we effectively have to fetch all the documents to get all
// the tags - which seems wasteful in itself already - so we should
// probably then use these docs to derive ourselves locally: any tagged
// search result, upcoming, untagged. That is, instead of doing a fetch for
// each of these, which unnecessarily burdens the server (and does slow
// requests). Or we don't do a tags cloud.
// Leaving it as is for now because 1) we don't do that many requests,
// 2) it's interesting to test various requests on our search handler.
allTags , err = h . fetchTags ( )
if err != nil {
httputil . ServeError ( w , r , err )
return
}
}
// fetch upcoming documents
upcoming , err := h . fetchDocuments ( maxDue , searchOpts { due : true } )
if err != nil {
httputil . ServeError ( w , r , err )
return
}
// fetch untagged documents
untagged , err := h . fetchDocuments ( limit , searchOpts { untagged : true } )
if err != nil {
httputil . ServeError ( w , r , err )
return
}
d := rootData {
BaseURL : baseURL ( r ) ,
Tags : strings . Join ( tags , ", " ) ,
Media : movm ,
SearchedDocs : searchedDocs ,
UntaggedDocs : MakeDocumentViewModels ( untagged ) ,
UpcomingDocs : MakeDocumentViewModels ( upcoming ) ,
TopMessage : template . HTML ( topMessage ) ,
ErrorMessage : errorMessage ,
AllTags : allTags ,
}
if err := rootTemplate . Execute ( w , d ) ; err != nil {
log . Printf ( "root template error: %v" , err )
httputil . ServeError ( w , r , err )
return
}
}
func baseURL ( r * http . Request ) string {
scheme := "http"
if r . TLS != nil {
scheme = "https"
}
return fmt . Sprintf ( "%s://%s%s" , scheme , r . Host , app . PathPrefix ( r ) )
}
func ( h * handler ) handleUploadURL ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "text/plain" )
fmt . Fprintf ( w , "%supload" , baseURL ( r ) )
return
}
func ( h * handler ) handleUpload ( w http . ResponseWriter , r * http . Request ) {
if r . Method != "POST" {
http . Error ( w , "not a POST" , http . StatusMethodNotAllowed )
return
}
mr , err := r . MultipartReader ( )
if err != nil {
httputil . ServeError ( w , r , err )
return
}
var br blob . Ref
var fileName string
var creation time . Time
cr := countingReader { }
for {
part , err := mr . NextPart ( )
if err == io . EOF {
break
}
if err != nil {
httputil . ServeError ( w , r , err )
return
}
name := part . FileName ( )
if name == "" {
// Let the user provide a creation time (for migrating Brad's data)
if part . FormName ( ) != "creation" {
log . Printf ( "form field %q provided in upload, but ignored" , part . FormName ( ) )
continue
}
creationParam , err := ioutil . ReadAll ( part )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not read provided creation time: %v" , err ) )
return
}
creation , err = time . Parse ( time . RFC3339 , string ( creationParam ) )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not parse provided creation time %q: %v" , creationParam , err ) )
return
}
continue
}
fileName = path . Base ( name )
cr . r = part
br , err = h . cl . UploadFile ( fileName , & cr , nil )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not write %v to blobserver: %v" , fileName , err ) )
return
}
}
creationProvided := true
if creation . IsZero ( ) {
creationProvided = false
creation = time . Now ( )
}
scan , err := h . fetchScanByContent ( br )
if err == nil {
if creationProvided {
newScan := scan
newScan . creation = creation
if err := h . updateScan ( scan . permanode , & newScan , & scan ) ; err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not update scan: %v" , err ) )
return
}
}
w . Write ( [ ] byte ( scan . permanode . String ( ) ) )
return
}
if ! os . IsNotExist ( err ) {
httputil . ServeError ( w , r , fmt . Errorf ( "could not check if scan with %v already exists: %v" , fileName , err ) )
return
}
scanRef , err := h . createScan ( mediaObject {
contentRef : br ,
creation : creation ,
} )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not create scan object for %v: %v" , fileName , err ) )
return
}
w . Write ( [ ] byte ( scanRef . String ( ) ) )
}
type countingReader struct {
hdr [ ] byte
n int
r io . Reader
}
func ( c * countingReader ) Read ( p [ ] byte ) ( int , error ) {
n , err := c . r . Read ( p )
if c . n < 1024 {
c . hdr = append ( c . hdr , p ... )
}
c . n += n
return n , err
}
func ( c * countingReader ) Mime ( ) string {
return magic . MIMEType ( c . hdr )
}
func ( h * handler ) handleResource ( w http . ResponseWriter , r * http . Request ) {
m := resourcePattern . FindStringSubmatch ( r . URL . Path )
if m == nil {
http . Error ( w , "invalid resource URL" , http . StatusBadRequest )
return
}
scanRef , ok := blob . Parse ( m [ 1 ] )
if ! ok {
http . Error ( w , fmt . Sprintf ( "invalid resource blobref: %q" , m [ 1 ] ) , http . StatusBadRequest )
return
}
mediaObject , err := h . fetchScan ( scanRef )
if err != nil {
if err == os . ErrNotExist {
http . Error ( w , fmt . Sprintf ( "%v not found" , scanRef ) , http . StatusNotFound )
return
}
httputil . ServeError ( w , r , fmt . Errorf ( "resource %v not found: %v" , scanRef , err ) )
return
}
// TODO(mpl): cache and thumbmeta
ih := & camliserver . ImageHandler {
Fetcher : h . cl ,
MaxWidth : search . MaxImageSize ,
MaxHeight : search . MaxImageSize ,
Square : false ,
// TODO(mpl): make the image pkg default to the below when ResizeSem is nil
ResizeSem : syncutil . NewSem ( constants . DefaultMaxResizeMem ) ,
}
if resizeParam := r . FormValue ( "resize" ) ; resizeParam != "" {
resized , err := strconv . Atoi ( resizeParam )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "bogus resize param %q: %v" , resizeParam , err ) )
return
}
ih . MaxWidth = resized
ih . MaxHeight = resized
}
ih . ServeHTTP ( w , r , mediaObject . contentRef )
return
}
func ( h * handler ) handleMakedoc ( w http . ResponseWriter , r * http . Request ) {
if r . Method != "POST" {
http . Error ( w , "not a POST" , http . StatusMethodNotAllowed )
return
}
// gather the media_ids from the form into scanKeys
if r . Form == nil {
r . ParseMultipartForm ( 1 )
}
refs := r . Form [ "blobref" ]
var pages [ ] blob . Ref
for _ , ref := range refs {
br , ok := blob . Parse ( ref )
if ! ok {
httputil . ServeError ( w , r , fmt . Errorf ( "invalid page blobRef %q" , ref ) )
return
}
pages = append ( pages , br )
}
var doc * document
var err error
doc , err = h . fetchDocumentByPages ( pages )
if err != nil {
if ! os . IsNotExist ( err ) {
httputil . ServeError ( w , r , fmt . Errorf ( "could not check if document already existed: %v" , err ) )
return
}
newDoc := document {
pages : pages ,
creation : time . Now ( ) ,
}
docRef , err := h . persistDocAndPages ( newDoc )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not create new document: %v" , err ) )
return
}
newDoc . permanode = docRef
doc = & newDoc
}
noUI := r . Form [ "noui" ]
if len ( noUI ) == 1 && noUI [ 0 ] == "1" {
// For when we just want to get the doc's blobRef as a response.
// Initially for migrating Brad's data, but can't hurt anyway.
w . Write ( [ ] byte ( doc . permanode . String ( ) ) )
return
}
http . Redirect ( w , r , fmt . Sprintf ( "%s%s?size=1200" , baseURL ( r ) , doc . displayURL ( ) ) , http . StatusFound )
}
func ( h * handler ) handleDoc ( w http . ResponseWriter , r * http . Request ) {
urlFields := strings . Split ( r . URL . Path , "/" )
if len ( urlFields ) < 3 {
http . Error ( w , "no document blobref" , http . StatusBadRequest )
return
}
docRef , ok := blob . Parse ( urlFields [ 2 ] )
if ! ok {
http . Error ( w , fmt . Sprintf ( "invalid document blobref: %q" , urlFields [ 2 ] ) , http . StatusBadRequest )
return
}
document , err := h . fetchDocument ( docRef )
if err != nil {
http . Redirect ( w , r , fmt . Sprintf ( "%s?error_message=DocRef+%s+not+found" , baseURL ( r ) , docRef ) , http . StatusFound )
return
}
var pages [ ] mediaObject
for _ , v := range document . pages {
// TODO(mpl): group fetch ?
page , err := h . fetchScan ( v )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not fetch page %v for document %v: %v" , v , document . permanode , err ) )
return
}
pages = append ( pages , page )
}
var size int
size = 1200
if sizeParam := r . FormValue ( "size" ) ; sizeParam != "" {
sizeint , err := strconv . Atoi ( sizeParam )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "invalide size param %q: %v" , sizeParam , err ) )
return
}
size = sizeint
}
show_single_list := size > 600
d := struct {
BaseURL string
Pages [ ] MediaObjectVM
Doc DocumentVM
ShowSingleList bool
Size int
} {
BaseURL : baseURL ( r ) ,
Pages : MakeMediaObjectViewModels ( pages ) ,
Doc : document . MakeViewModel ( ) ,
ShowSingleList : show_single_list ,
Size : size ,
}
if err := docTemplate . Execute ( w , d ) ; err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not serve doc template: %v" , err ) )
return
}
}
func ( h * handler ) handleChangedoc ( w http . ResponseWriter , r * http . Request ) {
if r . Method != "POST" {
http . Error ( w , "not a POST" , http . StatusMethodNotAllowed )
return
}
docRef , ok := blob . Parse ( r . FormValue ( "docref" ) )
if ! ok {
httputil . ServeError ( w , r , fmt . Errorf ( "invalid document blobRef %q" , r . FormValue ( "docref" ) ) )
return
}
mode := r . FormValue ( "mode" )
if mode == "break" {
if err := h . breakAndDeleteDoc ( docRef ) ; err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not delete document %v: %v" , docRef , err ) )
return
}
fmt . Fprintf ( w , "<html><body>[<< <a href='%s'>Back</a>] Doc %s deleted and images broken out as un-annotated.</body></html>" , baseURL ( r ) , docRef )
return
}
if mode == "delete" {
if err := h . deleteDocAndImages ( docRef ) ; err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not do full delete of %v: %v" , docRef , err ) )
return
}
fmt . Fprintf ( w , "<html><body>[<< <a href='%s'>Back</a>] Doc %s and its images deleted.</body></html>" , baseURL ( r ) , docRef )
return
}
document := & document { }
document . physicalLocation = r . FormValue ( "physical_location" )
document . title = r . FormValue ( "title" )
document . tags = newSeparatedString ( r . FormValue ( "tags" ) )
docDate , err := dateOrZero ( r . FormValue ( "date" ) , dateformatYyyyMmDd )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not assign new date to document: %v" , err ) )
return
}
document . docDate = docDate
duedate , err := dateOrZero ( r . FormValue ( "due_date" ) , dateformatYyyyMmDd )
if err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not assign new due date to document: %v" , err ) )
return
}
document . dueDate = duedate
if err := h . updateDocument ( docRef , document ) ; err != nil {
httputil . ServeError ( w , r , fmt . Errorf ( "could not update document %v: %v" , docRef , err ) )
return
}
http . Redirect ( w , r , fmt . Sprintf ( "%s?saved_doc=%s" , baseURL ( r ) , docRef ) , http . StatusFound )
}
func handleRobots ( w http . ResponseWriter , r * http . Request ) {
fmt . Fprint ( w , "User-agent: *\nDisallow: /\n" )
}
2017-03-19 05:27:00 +00:00
func handleUiFile ( w http . ResponseWriter , r * http . Request ) {
file := strings . TrimPrefix ( r . URL . Path , "/ui" )
root := uistatic . Files
f , err := root . Open ( "/" + file )
if err != nil {
http . NotFound ( w , r )
// TODO(stevearm): Replace all log.Printf with logf for proper prefixing
log . Printf ( "Failed to open file %v from embedded resources: %v" , file , err )
return
}
defer f . Close ( )
var modTime time . Time
if fi , err := f . Stat ( ) ; err == nil {
modTime = fi . ModTime ( )
}
if strings . HasSuffix ( file , ".css" ) {
w . Header ( ) . Set ( "Content-Type" , "text/css" )
} else if strings . HasSuffix ( file , ".js" ) {
w . Header ( ) . Set ( "Content-Type" , "application/javascript" )
}
http . ServeContent ( w , r , file , modTime , f )
}