mirror of https://github.com/writeas/writefreely
Merge pull request #172 from writeas/import-text
add basic text file imports Resolves T609pull/245/head
commit
75e2b60328
@ -0,0 +1,195 @@ |
|||||||
|
package writefreely |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"html/template" |
||||||
|
"io" |
||||||
|
"io/ioutil" |
||||||
|
"net/http" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror" |
||||||
|
"github.com/writeas/impart" |
||||||
|
wfimport "github.com/writeas/import" |
||||||
|
"github.com/writeas/web-core/log" |
||||||
|
) |
||||||
|
|
||||||
|
func viewImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error { |
||||||
|
// Fetch extra user data
|
||||||
|
p := NewUserPage(app, r, u, "Import Posts", nil) |
||||||
|
|
||||||
|
c, err := app.db.GetCollections(u, app.Config().App.Host) |
||||||
|
if err != nil { |
||||||
|
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("unable to fetch collections: %v", err)} |
||||||
|
} |
||||||
|
|
||||||
|
d := struct { |
||||||
|
*UserPage |
||||||
|
Collections *[]Collection |
||||||
|
Flashes []template.HTML |
||||||
|
Message string |
||||||
|
InfoMsg bool |
||||||
|
}{ |
||||||
|
UserPage: p, |
||||||
|
Collections: c, |
||||||
|
Flashes: []template.HTML{}, |
||||||
|
} |
||||||
|
|
||||||
|
flashes, _ := getSessionFlashes(app, w, r, nil) |
||||||
|
for _, flash := range flashes { |
||||||
|
if strings.HasPrefix(flash, "SUCCESS: ") { |
||||||
|
d.Message = strings.TrimPrefix(flash, "SUCCESS: ") |
||||||
|
} else if strings.HasPrefix(flash, "INFO: ") { |
||||||
|
d.Message = strings.TrimPrefix(flash, "INFO: ") |
||||||
|
d.InfoMsg = true |
||||||
|
} else { |
||||||
|
d.Flashes = append(d.Flashes, template.HTML(flash)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
showUserPage(w, "import", d) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func handleImport(app *App, u *User, w http.ResponseWriter, r *http.Request) error { |
||||||
|
// limit 10MB per submission
|
||||||
|
r.ParseMultipartForm(10 << 20) |
||||||
|
|
||||||
|
collAlias := r.PostFormValue("collection") |
||||||
|
coll := &Collection{ |
||||||
|
ID: 0, |
||||||
|
} |
||||||
|
var err error |
||||||
|
if collAlias != "" { |
||||||
|
coll, err = app.db.GetCollection(collAlias) |
||||||
|
if err != nil { |
||||||
|
log.Error("Unable to get collection for import: %s", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
// Only allow uploading to collection if current user is owner
|
||||||
|
if coll.OwnerID != u.ID { |
||||||
|
err := ErrUnauthorizedGeneral |
||||||
|
_ = addSessionFlash(app, w, r, err.Message, nil) |
||||||
|
return err |
||||||
|
} |
||||||
|
coll.hostName = app.cfg.App.Host |
||||||
|
} |
||||||
|
|
||||||
|
fileDates := make(map[string]int64) |
||||||
|
err = json.Unmarshal([]byte(r.FormValue("fileDates")), &fileDates) |
||||||
|
if err != nil { |
||||||
|
log.Error("invalid form data for file dates: %v", err) |
||||||
|
return impart.HTTPError{http.StatusBadRequest, "form data for file dates was invalid"} |
||||||
|
} |
||||||
|
files := r.MultipartForm.File["files"] |
||||||
|
var fileErrs []error |
||||||
|
filesSubmitted := len(files) |
||||||
|
var filesImported int |
||||||
|
for _, formFile := range files { |
||||||
|
fname := "" |
||||||
|
ok := func() bool { |
||||||
|
file, err := formFile.Open() |
||||||
|
if err != nil { |
||||||
|
fileErrs = append(fileErrs, fmt.Errorf("Unable to read file %s", formFile.Filename)) |
||||||
|
log.Error("import file: open from form: %v", err) |
||||||
|
return false |
||||||
|
} |
||||||
|
defer file.Close() |
||||||
|
|
||||||
|
tempFile, err := ioutil.TempFile("", "post-upload-*.txt") |
||||||
|
if err != nil { |
||||||
|
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename)) |
||||||
|
log.Error("import file: create temp file %s: %v", formFile.Filename, err) |
||||||
|
return false |
||||||
|
} |
||||||
|
defer tempFile.Close() |
||||||
|
|
||||||
|
_, err = io.Copy(tempFile, file) |
||||||
|
if err != nil { |
||||||
|
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename)) |
||||||
|
log.Error("import file: copy to temp location %s: %v", formFile.Filename, err) |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
info, err := tempFile.Stat() |
||||||
|
if err != nil { |
||||||
|
fileErrs = append(fileErrs, fmt.Errorf("Internal error for %s", formFile.Filename)) |
||||||
|
log.Error("import file: stat temp file %s: %v", formFile.Filename, err) |
||||||
|
return false |
||||||
|
} |
||||||
|
fname = info.Name() |
||||||
|
return true |
||||||
|
}() |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
post, err := wfimport.FromFile(filepath.Join(os.TempDir(), fname)) |
||||||
|
if err == wfimport.ErrEmptyFile { |
||||||
|
// not a real error so don't log
|
||||||
|
_ = addSessionFlash(app, w, r, fmt.Sprintf("%s was empty, import skipped", formFile.Filename), nil) |
||||||
|
continue |
||||||
|
} else if err == wfimport.ErrInvalidContentType { |
||||||
|
// same as above
|
||||||
|
_ = addSessionFlash(app, w, r, fmt.Sprintf("%s is not a supported post file", formFile.Filename), nil) |
||||||
|
continue |
||||||
|
} else if err != nil { |
||||||
|
fileErrs = append(fileErrs, fmt.Errorf("failed to read copy of %s", formFile.Filename)) |
||||||
|
log.Error("import textfile: file to post: %v", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
if collAlias != "" { |
||||||
|
post.Collection = collAlias |
||||||
|
} |
||||||
|
dateTime := time.Unix(fileDates[formFile.Filename], 0) |
||||||
|
post.Created = &dateTime |
||||||
|
created := post.Created.Format("2006-01-02T15:04:05Z") |
||||||
|
submittedPost := SubmittedPost{ |
||||||
|
Title: &post.Title, |
||||||
|
Content: &post.Content, |
||||||
|
Font: "norm", |
||||||
|
Created: &created, |
||||||
|
} |
||||||
|
rp, err := app.db.CreatePost(u.ID, coll.ID, &submittedPost) |
||||||
|
if err != nil { |
||||||
|
fileErrs = append(fileErrs, fmt.Errorf("failed to create post from %s", formFile.Filename)) |
||||||
|
log.Error("import textfile: create db post: %v", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Federate post, if necessary
|
||||||
|
if app.cfg.App.Federation && coll.ID > 0 { |
||||||
|
go federatePost( |
||||||
|
app, |
||||||
|
&PublicPost{ |
||||||
|
Post: rp, |
||||||
|
Collection: &CollectionObj{ |
||||||
|
Collection: *coll, |
||||||
|
}, |
||||||
|
}, |
||||||
|
coll.ID, |
||||||
|
false, |
||||||
|
) |
||||||
|
} |
||||||
|
filesImported++ |
||||||
|
} |
||||||
|
if len(fileErrs) != 0 { |
||||||
|
_ = addSessionFlash(app, w, r, multierror.ListFormatFunc(fileErrs), nil) |
||||||
|
} |
||||||
|
|
||||||
|
if filesImported == filesSubmitted { |
||||||
|
verb := "posts" |
||||||
|
if filesSubmitted == 1 { |
||||||
|
verb = "post" |
||||||
|
} |
||||||
|
_ = addSessionFlash(app, w, r, fmt.Sprintf("SUCCESS: Import complete, %d %s imported.", filesImported, verb), nil) |
||||||
|
} else if filesImported > 0 { |
||||||
|
_ = addSessionFlash(app, w, r, fmt.Sprintf("INFO: %d of %d posts imported, see details below.", filesImported, filesSubmitted), nil) |
||||||
|
} |
||||||
|
return impart.HTTPError{http.StatusFound, "/me/import"} |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
{{define "import"}} |
||||||
|
{{template "header" .}} |
||||||
|
<style> |
||||||
|
input[type=file] { |
||||||
|
padding: 0; |
||||||
|
font-size: 0.86em; |
||||||
|
display: block; |
||||||
|
margin: 0.5rem 0; |
||||||
|
} |
||||||
|
label { |
||||||
|
display: block; |
||||||
|
margin: 1em 0; |
||||||
|
} |
||||||
|
</style> |
||||||
|
|
||||||
|
<div class="snug content-container"> |
||||||
|
<h1 id="import-header">Import posts</h1> |
||||||
|
{{if .Message}} |
||||||
|
<div class="alert {{if .InfoMsg}}info{{else}}success{{end}}"> |
||||||
|
<p>{{.Message}}</p> |
||||||
|
</div> |
||||||
|
{{end}} |
||||||
|
{{if .Flashes}} |
||||||
|
<ul class="errors"> |
||||||
|
{{range .Flashes}}<li class="urgent">{{.}}</li>{{end}} |
||||||
|
</ul> |
||||||
|
{{end}} |
||||||
|
<p>Publish plain text or Markdown files to your account by uploading them below.</p> |
||||||
|
<div class="formContainer"> |
||||||
|
<form id="importPosts" class="prominent" enctype="multipart/form-data" action="/api/me/import" method="POST"> |
||||||
|
<label>Select some files to import: |
||||||
|
<input id="fileInput" class="fileInput" name="files" type="file" multiple accept="text/markdown, text/plain"/> |
||||||
|
</label> |
||||||
|
<input id="fileDates" name="fileDates" hidden/> |
||||||
|
<label> |
||||||
|
Import these posts to: |
||||||
|
<select name="collection"> |
||||||
|
{{range $i, $el := .Collections}} |
||||||
|
<option value="{{.Alias}}" {{if eq $i 0}}selected{{end}}>{{.DisplayTitle}}</option> |
||||||
|
{{end}} |
||||||
|
<option value="">Drafts</option> |
||||||
|
</select> |
||||||
|
</label> |
||||||
|
<script> |
||||||
|
const fileInput = document.getElementById('fileInput'); |
||||||
|
const fileDates = document.getElementById('fileDates'); |
||||||
|
fileInput.addEventListener('change', (e) => { |
||||||
|
const files = e.target.files; |
||||||
|
let dateMap = {}; |
||||||
|
for (let file of files) { |
||||||
|
dateMap[file.name] = file.lastModified / 1000; |
||||||
|
} |
||||||
|
fileDates.value = JSON.stringify(dateMap); |
||||||
|
}) |
||||||
|
</script> |
||||||
|
<input type="submit" value="Import" /> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
{{template "footer" .}} |
||||||
|
{{end}} |
Loading…
Reference in new issue