This repository was archived by the owner on Jul 2, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 139
[WIP] Download annotated notebooks #34
Closed
Closed
Changes from 42 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
713b03f
Merge remote-tracking branch 'upstream/master'
peerdavid 1660c61
Merge remote-tracking branch 'origin/master'
peerdavid b23b692
Changes to implement geta
peerdavid cc06bd9
Finished geta for svg files
peerdavid f967740
Finished geta for svg
peerdavid 01cf2b4
Own dir for notebook
peerdavid 09d6c6d
Move into own directory
peerdavid 9e48489
Fixed exportNotebook script for rmapi usage
peerdavid 68ebecd
Export of annotated pdf
peerdavid 4038663
Pdf export for pdf files
peerdavid 48f65d1
Updated documentation
peerdavid ff515f6
Change to line2svg
peerdavid 2417149
Fix for 1.6 with rM2svg
peerdavid 238f38c
Fixed for RM version 1.6
peerdavid 5d635b3
Using invisible tmp folders
peerdavid 6aea236
Test with new rM2svg script
peerdavid d78292c
Minor changes
peerdavid 1f257b2
Fixes for notebook without background pdfs
peerdavid c29be12
Support templates
peerdavid 94d1be9
Updated colors and docu
peerdavid b805445
Deleted unused code
peerdavid f3d4542
Removed logging statements
peerdavid a57cbaa
Added mgeta to get a folder annotated
peerdavid ba6d0a5
Updated readme for mgeta
peerdavid 5662904
Minor changes
peerdavid 3b978a1
Test with different svg to pdf conversion
peerdavid ac4b5c0
Test with inkscape
peerdavid a45c4fd
Store template in template tmp folder
peerdavid 982d8f2
Using rsvg-convert as its faster.
peerdavid cb7771f
Update README.md
peerdavid cbc6c0c
Update README.md
peerdavid b09390d
Update README.md
peerdavid 2315caa
Prepare for PR
peerdavid c6b6aa0
Merge remote-tracking branch 'origin/master'
peerdavid 0dfee84
Update annotated pdf file only if modified date time changed.
peerdavid 43bc787
Better logging
peerdavid f872dfd
Improved performance
peerdavid 0c857d9
Fixed invalid logging
peerdavid ea56392
Ignore epub files and simply move pdfs that are not annotated.
peerdavid e434018
Removed dependency to pdfunite
peerdavid 9fbfb30
1. Added verification for geta. If tools are missing geta and mgeta a…
peerdavid b36b097
Rotate annotations whenever the pdf file is rotated
peerdavid 964ed36
Minor fix
peerdavid 3ca7ec1
uising github.com/peerdavid; fixed 1.7 bug with missing page count
peerdavid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,255 @@ | ||
| package shell | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "time" | ||
| "os/exec" | ||
| "strings" | ||
| "archive/zip" | ||
| "github.com/juruen/rmapi/log" | ||
| "os" | ||
| "io" | ||
| "github.com/abiosoft/ishell" | ||
| "path/filepath" | ||
| "github.com/juruen/rmapi/model" | ||
| ) | ||
|
|
||
|
|
||
| func verifyGetaCmdTools() [] string { | ||
| var missingTools [] string | ||
|
|
||
| missingTools = verifyCommand("python3", missingTools) | ||
| missingTools = verifyCommand("convert", missingTools) | ||
| missingTools = verifyCommand("rsvg-convert", missingTools) | ||
| missingTools = verifyCommand("pdftk", missingTools) | ||
| missingTools = verifyCommand("pdf270", missingTools) | ||
|
|
||
| return missingTools | ||
| } | ||
|
|
||
|
|
||
| // See also https://siongui.github.io/2018/03/16/go-check-if-command-exists/ | ||
| func verifyCommand(name string, missingTools [] string) [] string { | ||
| cmd := exec.Command("/bin/sh", "-c", "command -v "+name) | ||
| if err := cmd.Run(); err != nil { | ||
| return append(missingTools, name) | ||
| } | ||
| return missingTools | ||
| } | ||
|
|
||
|
|
||
| func getaCmd(ctx *ShellCtxt) *ishell.Cmd { | ||
| return &ishell.Cmd{ | ||
| Name: "geta", | ||
| Help: "copy remote file to local with annotations.", | ||
| Completer: createEntryCompleter(ctx), | ||
| Func: func(c *ishell.Context) { | ||
| // Parse cmd args | ||
| if len(c.Args) == 0 { | ||
| c.Err(errors.New("missing source file")) | ||
| return | ||
| } | ||
| srcName := c.Args[0] | ||
|
|
||
| // Download document as zip | ||
| node, err := ctx.api.Filetree.NodeByPath(srcName, ctx.node) | ||
| if err != nil || node.IsDirectory() { | ||
| c.Err(errors.New("file doesn't exist")) | ||
| return | ||
| } | ||
|
|
||
| err = getAnnotatedDocument(ctx, node, "") | ||
| if err != nil { | ||
| c.Err(err) | ||
| return | ||
| } | ||
|
|
||
| fmt.Println("OK") | ||
| }, | ||
| } | ||
| } | ||
|
|
||
|
|
||
| func getAnnotatedDocument(ctx *ShellCtxt, node *model.Node, path string) error { | ||
| zipFile := fmt.Sprintf(".%s.zip", node.Name()) | ||
|
|
||
| // Set output name and output file name | ||
| output := node.Name() | ||
| if(path != ""){ | ||
| output = fmt.Sprintf("%s/%s", path, node.Name()) | ||
| } | ||
| outputFileName := fmt.Sprintf("%s.pdf", output) | ||
|
|
||
| // Parse last modified time of document on api | ||
| modifiedClientTime, err := time.Parse(time.RFC3339Nano, node.Document.ModifiedClient) | ||
| if err != nil { | ||
| // If we could not parse the time correctly, we still continue | ||
| // with the execution such that the pdf is downloaded... | ||
| log.Warning.Println("Could not parse modified time. Overwrite existing file.") | ||
| modifiedClientTime = time.Now().Local() | ||
| } | ||
|
|
||
| // If document has not changed since last update skip pdf convertion | ||
| outputFile, err := os.Stat(outputFileName) | ||
| if !os.IsNotExist(err) { | ||
| outputFileModTime := outputFile.ModTime() | ||
| if(outputFileModTime.Equal(modifiedClientTime)){ | ||
| log.Trace.Println("Nothing changed since last download. Skip. ") | ||
| cleanup("", zipFile); | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // Download document if content has changed | ||
| err = ctx.api.FetchDocument(node.Document.ID, zipFile) | ||
| if err != nil { | ||
| return errors.New(fmt.Sprintf("Failed to download file %s with %s", node.Name(), err.Error())) | ||
| } | ||
|
|
||
| // Unzip document | ||
| tmpFolder := fmt.Sprintf(".%s", node.Document.ID) | ||
| _, err = unzip(zipFile, tmpFolder) | ||
| if err != nil { | ||
| cleanup(tmpFolder, zipFile); | ||
| return err | ||
| } | ||
|
|
||
| // Currently we don't support the annotation of epub files | ||
| if(isEpub(tmpFolder, node)){ | ||
| cleanup(tmpFolder, zipFile); | ||
| return errors.New("Could not annotate epub files."); | ||
| } | ||
|
|
||
| // If pdf is annotated convert it, otherwise move the pdf file | ||
| if(documentIsAnnotated(tmpFolder, node)){ | ||
| exportPdf := os.Getenv("GOPATH") + "/src/github.com/juruen/rmapi/tools/exportAnnotatedPdf" | ||
| rM2svg := os.Getenv("GOPATH") + "/src/github.com/juruen/rmapi/tools/rM2svg" | ||
| output, err := exec.Command( | ||
| "/bin/bash", | ||
| exportPdf, | ||
| tmpFolder, | ||
| node.Document.ID, | ||
| output, | ||
| rM2svg).CombinedOutput() | ||
|
|
||
| log.Trace.Println(fmt.Sprintf("%s", output)) | ||
|
|
||
| if err != nil { | ||
| cleanup(tmpFolder, zipFile); | ||
| return err | ||
| } | ||
|
|
||
| } else { | ||
| log.Trace.Println("Document is not annotated. Move original file.") | ||
|
|
||
| // Two cases exist: Pdf file or a notebook. The second case | ||
| // Could not occur because a notebook has always at least one page. | ||
| // Therefore we know that its a pdf file. | ||
| plainPdfFile := fmt.Sprintf("%s/%s.pdf", tmpFolder, node.Document.ID) | ||
| err := os.Rename(plainPdfFile, outputFileName) | ||
| if err != nil { | ||
| log.Error.Println("Could not move pdf file.") | ||
| cleanup(tmpFolder, zipFile); | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| // Set creation time | ||
| err = os.Chtimes(outputFileName, modifiedClientTime, modifiedClientTime) | ||
| if err != nil { | ||
| fmt.Println("(Warning) Could not set modified time of pdf file.") | ||
| } | ||
|
|
||
| // Cleanup | ||
| cleanup(tmpFolder, zipFile); | ||
| return nil | ||
| } | ||
|
|
||
|
|
||
| func cleanup(tmpFolder string, zipFile string) { | ||
| _, err := os.Stat(tmpFolder); | ||
| if(!os.IsNotExist(err)){ | ||
| os.RemoveAll(tmpFolder) | ||
| } | ||
|
|
||
| _, err = os.Stat(zipFile); | ||
| if(!os.IsNotExist(err)){ | ||
| os.Remove(zipFile) | ||
| } | ||
| } | ||
|
|
||
|
|
||
| // From https://golangcode.com/unzip-files-in-go/ | ||
| func unzip(src string, dest string) ([]string, error) { | ||
|
|
||
| var filenames []string | ||
|
|
||
| r, err := zip.OpenReader(src) | ||
| if err != nil { | ||
| return filenames, err | ||
| } | ||
| defer r.Close() | ||
|
|
||
| for _, f := range r.File { | ||
|
|
||
| rc, err := f.Open() | ||
| if err != nil { | ||
| return filenames, err | ||
| } | ||
| defer rc.Close() | ||
|
|
||
| // Store filename/path for returning and using later on | ||
| fpath := filepath.Join(dest, f.Name) | ||
|
|
||
| // Check for ZipSlip. More Info: http://bit.ly/2MsjAWE | ||
| if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { | ||
| return filenames, fmt.Errorf("%s: illegal file path", fpath) | ||
| } | ||
|
|
||
| filenames = append(filenames, fpath) | ||
|
|
||
| if f.FileInfo().IsDir() { | ||
|
|
||
| // Make Folder | ||
| os.MkdirAll(fpath, os.ModePerm) | ||
|
|
||
| } else { | ||
|
|
||
| // Make File | ||
| if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { | ||
| return filenames, err | ||
| } | ||
|
|
||
| outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) | ||
| if err != nil { | ||
| return filenames, err | ||
| } | ||
|
|
||
| _, err = io.Copy(outFile, rc) | ||
|
|
||
| // Close the file without defer to close before next iteration of loop | ||
| outFile.Close() | ||
|
|
||
| if err != nil { | ||
| return filenames, err | ||
| } | ||
|
|
||
| } | ||
| } | ||
| return filenames, nil | ||
| } | ||
|
|
||
|
|
||
| func documentIsAnnotated(tmpFolder string, node *model.Node) bool { | ||
| annotationFolder := fmt.Sprintf("%s/%s", tmpFolder, node.Document.ID) | ||
| _, err := os.Stat(annotationFolder); | ||
| return !os.IsNotExist(err) | ||
| } | ||
|
|
||
|
|
||
| func isEpub(tmpFolder string, node *model.Node) bool { | ||
| annotationFolder := fmt.Sprintf("%s/%s.epub", tmpFolder, node.Document.ID) | ||
| _, err := os.Stat(annotationFolder); | ||
| return !os.IsNotExist(err) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| package shell | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path" | ||
|
|
||
| "github.com/abiosoft/ishell" | ||
| "github.com/juruen/rmapi/filetree" | ||
| "github.com/juruen/rmapi/model" | ||
| ) | ||
|
|
||
| func mgetaCmd(ctx *ShellCtxt) *ishell.Cmd { | ||
| return &ishell.Cmd{ | ||
| Name: "mgeta", | ||
| Help: "recursively copy remote directory to local with annotations", | ||
| Completer: createDirCompleter(ctx), | ||
| Func: func(c *ishell.Context) { | ||
| if len(c.Args) == 0 { | ||
| c.Err(errors.New(("missing source dir"))) | ||
| return | ||
| } | ||
|
|
||
| srcName := c.Args[0] | ||
|
|
||
| node, err := ctx.api.Filetree.NodeByPath(srcName, ctx.node) | ||
|
|
||
| if err != nil || node.IsFile() { | ||
| c.Err(errors.New("directory doesn't exist")) | ||
| return | ||
| } | ||
|
|
||
| visitor := filetree.FileTreeVistor{ | ||
| func(currentNode *model.Node, currentPath []string) bool { | ||
| idxDir := 0 | ||
| if srcName == "." && len(currentPath) > 0 { | ||
| idxDir = 1 | ||
| } | ||
|
|
||
| dst := "./" + filetree.BuildPath(currentPath[idxDir:], currentNode.Name()) | ||
| dir := path.Dir(dst) | ||
| os.MkdirAll(dir, 0766) | ||
|
|
||
| if currentNode.IsDirectory() { | ||
| return filetree.ContinueVisiting | ||
| } | ||
|
|
||
| c.Printf("downloading [%s]...", dst) | ||
|
|
||
| err = getAnnotatedDocument(ctx, currentNode, fmt.Sprintf("%s", dir)) | ||
|
|
||
| if err == nil { | ||
| c.Println(" OK") | ||
| return filetree.ContinueVisiting | ||
| } | ||
|
|
||
| c.Err(errors.New(fmt.Sprintf("Failed to downlaod file %s", currentNode.Name()))) | ||
|
|
||
| return filetree.ContinueVisiting | ||
| }, | ||
| } | ||
|
|
||
| filetree.WalkTree(node, visitor) | ||
| }, | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can drop this line :)