2020-06-24 14:01:51 +00:00
package fs
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
2020-06-26 18:07:21 +00:00
"os"
2020-06-24 14:01:51 +00:00
"path/filepath"
"strconv"
2020-06-25 20:31:58 +00:00
"strings"
2020-06-26 18:07:21 +00:00
"time"
2020-06-24 14:01:51 +00:00
2020-07-03 19:20:56 +00:00
"github.com/bouncepaw/mycorrhiza/mycelium"
2020-06-30 18:13:46 +00:00
"github.com/bouncepaw/mycorrhiza/util"
2020-06-24 14:01:51 +00:00
)
type Hypha struct {
Exists bool ` json:"-" `
FullName string ` json:"-" `
ViewCount int ` json:"views" `
Deleted bool ` json:"deleted" `
Revisions map [ string ] * Revision ` json:"revisions" `
actual * Revision ` json:"-" `
2020-07-02 19:03:30 +00:00
Invalid bool ` json:"-" `
Err error ` json:"-" `
2020-06-26 18:07:21 +00:00
}
func ( h * Hypha ) Invalidate ( err error ) * Hypha {
h . Invalid = true
h . Err = err
return h
}
2020-07-03 19:20:56 +00:00
func ( s * Storage ) OpenFromMap ( m map [ string ] string ) * Hypha {
name := mycelium . NameWithMyceliumInMap ( m )
h := s . open ( name )
if rev , ok := m [ "rev" ] ; ok {
return h . OnRevision ( rev )
}
return h
}
func ( s * Storage ) open ( name string ) * Hypha {
2020-06-30 18:13:46 +00:00
name = util . UrlToCanonical ( name )
2020-06-24 14:01:51 +00:00
h := & Hypha {
Exists : true ,
2020-06-30 18:13:46 +00:00
FullName : util . CanonicalToDisplay ( name ) ,
2020-06-24 14:01:51 +00:00
}
path , ok := s . paths [ name ]
// This hypha does not exist yet
if ! ok {
log . Println ( "Hypha" , name , "does not exist" )
h . Exists = false
h . Revisions = make ( map [ string ] * Revision )
} else {
metaJsonText , err := ioutil . ReadFile ( filepath . Join ( path , "meta.json" ) )
if err != nil {
2020-06-26 18:07:21 +00:00
return h . Invalidate ( err )
2020-06-24 14:01:51 +00:00
}
err = json . Unmarshal ( metaJsonText , & h )
if err != nil {
2020-06-26 18:07:21 +00:00
return h . Invalidate ( err )
2020-06-24 14:01:51 +00:00
}
// fill in rooted paths to content files and full names
for idStr , rev := range h . Revisions {
rev . FullName = filepath . Join ( h . parentName ( ) , rev . ShortName )
rev . Id , _ = strconv . Atoi ( idStr )
if rev . BinaryName != "" {
rev . BinaryPath = filepath . Join ( path , rev . BinaryName )
}
rev . TextPath = filepath . Join ( path , rev . TextName )
}
2020-06-26 18:07:21 +00:00
return h . OnRevision ( "0" )
2020-06-24 14:01:51 +00:00
}
2020-06-26 18:07:21 +00:00
return h
2020-06-24 14:01:51 +00:00
}
// OnRevision tries to change to a revision specified by `id`.
2020-06-26 18:07:21 +00:00
func ( h * Hypha ) OnRevision ( id string ) * Hypha {
if h . Invalid || ! h . Exists {
return h
}
2020-06-24 14:01:51 +00:00
if len ( h . Revisions ) == 0 {
2020-06-26 18:07:21 +00:00
return h . Invalidate ( errors . New ( "This hypha has no revisions" ) )
2020-06-24 14:01:51 +00:00
}
if id == "0" {
id = h . NewestId ( )
}
// Revision must be there, so no error checking
if rev , _ := h . Revisions [ id ] ; true {
h . actual = rev
}
2020-06-26 18:07:21 +00:00
return h
2020-06-24 14:01:51 +00:00
}
func ( h * Hypha ) PlainLog ( s string ) {
2020-06-25 17:18:50 +00:00
if h . Exists {
log . Println ( h . FullName , h . actual . Id , s )
} else {
log . Println ( "nonexistent" , h . FullName , s )
}
2020-06-24 14:01:51 +00:00
}
2020-06-27 17:47:53 +00:00
func ( h * Hypha ) LogSuccMaybe ( succMsg string ) * Hypha {
if h . Invalid {
h . PlainLog ( h . Err . Error ( ) )
} else {
h . PlainLog ( succMsg )
2020-06-24 14:01:51 +00:00
}
2020-06-27 17:47:53 +00:00
return h
2020-06-24 16:19:44 +00:00
}
2020-06-24 14:01:51 +00:00
// ActionRaw is used with `?action=raw`.
// It writes text content of the revision without any parsing or rendering.
2020-06-27 17:47:53 +00:00
func ( h * Hypha ) ActionRaw ( w http . ResponseWriter ) * Hypha {
if h . Invalid {
return h
}
2020-07-03 19:20:56 +00:00
if h . Exists {
fileContents , err := ioutil . ReadFile ( h . actual . TextPath )
if err != nil {
return h . Invalidate ( err )
}
w . Header ( ) . Set ( "Content-Type" , h . mimeTypeForActionRaw ( ) )
w . WriteHeader ( http . StatusOK )
w . Write ( fileContents )
} else {
log . Println ( "Hypha" , h . FullName , "has no actual revision" )
w . WriteHeader ( http . StatusNotFound )
2020-06-24 14:01:51 +00:00
}
2020-06-27 17:47:53 +00:00
return h
2020-06-24 14:01:51 +00:00
}
2020-06-25 17:18:50 +00:00
// ActionBinary is used with `?action=binary`.
2020-06-24 14:01:51 +00:00
// It writes contents of binary content file.
2020-06-27 17:47:53 +00:00
func ( h * Hypha ) ActionBinary ( w http . ResponseWriter ) * Hypha {
if h . Invalid {
return h
}
2020-07-03 19:20:56 +00:00
if h . Exists {
fileContents , err := ioutil . ReadFile ( h . actual . BinaryPath )
if err != nil {
return h . Invalidate ( err )
}
w . Header ( ) . Set ( "Content-Type" , h . actual . BinaryMime )
w . WriteHeader ( http . StatusOK )
w . Write ( fileContents )
} else {
log . Println ( "Hypha" , h . FullName , "has no actual revision" )
w . WriteHeader ( http . StatusNotFound )
2020-06-24 14:01:51 +00:00
}
2020-06-27 17:47:53 +00:00
return h
2020-06-24 14:01:51 +00:00
}
2020-06-24 16:19:44 +00:00
// ActionZen is used with `?action=zen`.
// It renders the hypha but without any layout or styles. Pure. Zen.
2020-06-27 17:47:53 +00:00
func ( h * Hypha ) ActionZen ( w http . ResponseWriter ) * Hypha {
if h . Invalid {
return h
}
2020-06-24 16:19:44 +00:00
html , err := h . asHtml ( )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
2020-06-27 17:47:53 +00:00
return h . Invalidate ( err )
2020-06-24 16:19:44 +00:00
}
w . Header ( ) . Set ( "Content-Type" , "text/html;charset=utf-8" )
w . WriteHeader ( http . StatusOK )
w . Write ( [ ] byte ( html ) )
2020-06-27 17:47:53 +00:00
return h
2020-06-24 16:19:44 +00:00
}
2020-06-25 17:18:50 +00:00
// ActionView is used with `?action=view` or no action at all.
// It renders the page, the layout and everything else.
2020-06-28 15:02:07 +00:00
func ( h * Hypha ) ActionView ( w http . ResponseWriter , renderExists , renderNotExists func ( string , string ) [ ] byte ) * Hypha {
2020-06-25 17:18:50 +00:00
var html string
var err error
if h . Exists {
html , err = h . asHtml ( )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
2020-06-27 17:47:53 +00:00
return h . Invalidate ( err )
2020-06-25 17:18:50 +00:00
}
}
w . Header ( ) . Set ( "Content-Type" , "text/html;charset=utf-8" )
w . WriteHeader ( http . StatusOK )
if h . Exists {
2020-06-28 15:02:07 +00:00
w . Write ( renderExists ( h . FullName , html ) )
2020-06-25 17:18:50 +00:00
} else {
2020-06-28 15:02:07 +00:00
w . Write ( renderNotExists ( h . FullName , "" ) )
2020-06-25 17:18:50 +00:00
}
2020-06-27 17:47:53 +00:00
return h
2020-06-25 17:18:50 +00:00
}
2020-06-26 18:07:21 +00:00
// CreateDirIfNeeded creates directory where the hypha must reside if needed.
// It is not needed if the dir already exists.
func ( h * Hypha ) CreateDirIfNeeded ( ) * Hypha {
if h . Invalid {
return h
}
// os.MkdirAll created dir if it is not there. Basically, checks it for us.
2020-07-02 19:03:30 +00:00
err := os . MkdirAll ( h . Path ( ) , os . ModePerm )
2020-06-26 18:07:21 +00:00
if err != nil {
h . Invalidate ( err )
}
return h
}
// makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant.
// Expected output for string above: []string{"foo", "bar", "kek"}
func makeTagsSlice ( responseTagsString string ) ( ret [ ] string ) {
for _ , tag := range strings . Split ( responseTagsString , "," ) {
if trimmed := strings . TrimSpace ( tag ) ; "" == trimmed {
ret = append ( ret , trimmed )
}
}
return ret
}
// revisionFromHttpData creates a new revison for hypha `h`. All data is fetched from `rq`, except for BinaryMime and BinaryPath which require additional processing. The revision is inserted for you. You'll have to pop it out if there is an error.
func ( h * Hypha ) AddRevisionFromHttpData ( rq * http . Request ) * Hypha {
if h . Invalid {
return h
}
id := 1
if h . Exists {
id = h . actual . Id + 1
}
log . Printf ( "Creating revision %d from http data" , id )
rev := & Revision {
Id : id ,
FullName : h . FullName ,
ShortName : filepath . Base ( h . FullName ) ,
Tags : makeTagsSlice ( rq . PostFormValue ( "tags" ) ) ,
Comment : rq . PostFormValue ( "comment" ) ,
Author : rq . PostFormValue ( "author" ) ,
Time : int ( time . Now ( ) . Unix ( ) ) ,
TextMime : rq . PostFormValue ( "text_mime" ) ,
// Fields left: BinaryMime, BinaryPath, BinaryName, TextName, TextPath
}
rev . generateTextFilename ( ) // TextName is set now
rev . TextPath = filepath . Join ( h . Path ( ) , rev . TextName )
return h . AddRevision ( rev )
}
func ( h * Hypha ) AddRevision ( rev * Revision ) * Hypha {
if h . Invalid {
return h
}
h . Revisions [ strconv . Itoa ( rev . Id ) ] = rev
h . actual = rev
return h
}
// WriteTextFileFromHttpData tries to fetch text content from `rq` for revision `rev` and write it to a corresponding text file. It used in `HandlerUpdate`.
func ( h * Hypha ) WriteTextFileFromHttpData ( rq * http . Request ) * Hypha {
if h . Invalid {
return h
}
data := [ ] byte ( rq . PostFormValue ( "text" ) )
err := ioutil . WriteFile ( h . TextPath ( ) , data , 0644 )
if err != nil {
log . Println ( "Failed to write" , len ( data ) , "bytes to" , h . TextPath ( ) )
h . Invalidate ( err )
}
return h
}
// WriteBinaryFileFromHttpData tries to fetch binary content from `rq` for revision `newRev` and write it to a corresponding binary file. If there is no content, it is taken from a previous revision, if there is any.
func ( h * Hypha ) WriteBinaryFileFromHttpData ( rq * http . Request ) * Hypha {
if h . Invalid {
return h
}
// 10 MB file size limit
rq . ParseMultipartForm ( 10 << 20 )
// Read file
file , handler , err := rq . FormFile ( "binary" )
if file != nil {
defer file . Close ( )
}
// If file is not passed:
if err != nil {
// Let's hope there are no other errors 🙏
// TODO: actually check if there any other errors
log . Println ( "No binary data passed for" , h . FullName )
// It is expected there is at least one revision
if len ( h . Revisions ) > 1 {
prevRev := h . Revisions [ strconv . Itoa ( h . actual . Id - 1 ) ]
h . actual . BinaryMime = prevRev . BinaryMime
h . actual . BinaryPath = prevRev . BinaryPath
h . actual . BinaryName = prevRev . BinaryName
log . Println ( "Set previous revision's binary data" )
}
return h
}
// If file is passed:
h . actual . BinaryMime = handler . Header . Get ( "Content-Type" )
h . actual . generateBinaryFilename ( )
h . actual . BinaryPath = filepath . Join ( h . Path ( ) , h . actual . BinaryName )
data , err := ioutil . ReadAll ( file )
if err != nil {
return h . Invalidate ( err )
}
log . Println ( "Got" , len ( data ) , "of binary data for" , h . FullName )
err = ioutil . WriteFile ( h . actual . BinaryPath , data , 0644 )
if err != nil {
return h . Invalidate ( err )
}
log . Println ( "Written" , len ( data ) , "of binary data for" , h . FullName )
return h
}
// SaveJson dumps the hypha's metadata to `meta.json` file.
func ( h * Hypha ) SaveJson ( ) * Hypha {
if h . Invalid {
return h
}
data , err := json . MarshalIndent ( h , "" , "\t" )
if err != nil {
return h . Invalidate ( err )
}
err = ioutil . WriteFile ( h . MetaJsonPath ( ) , data , 0644 )
if err != nil {
return h . Invalidate ( err )
}
log . Println ( "Saved JSON data of" , h . FullName )
return h
}
// Store adds `h` to the `Hs` if it is not already there
func ( h * Hypha ) Store ( ) * Hypha {
if ! h . Invalid {
2020-07-02 19:03:30 +00:00
Hs . paths [ h . CanonicalName ( ) ] = h . Path ( )
2020-06-26 18:07:21 +00:00
}
return h
}