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