245 lines
6.7 KiB
Go
245 lines
6.7 KiB
Go
package sync
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/base32"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"srs.epita.fr/fic-server/libfic"
|
|
|
|
"golang.org/x/crypto/blake2b"
|
|
)
|
|
|
|
// Importer are abstract methods required to import challenges.
|
|
type Importer interface {
|
|
// Kind returns information about the Importer, for human interrest.
|
|
Kind() string
|
|
// Id returns information about the current state (commit id, ...).
|
|
Id() *string
|
|
// init performs the importer initialization.
|
|
Init() error
|
|
// sync tries to pull the latest modification of the underlying storage.
|
|
Sync() error
|
|
// Exists checks if the given location exists from the Importer point of view.
|
|
Exists(filename string) bool
|
|
// toURL gets the full path/URL to the given file, the Importer will look internaly (used for debuging purpose).
|
|
toURL(filename string) string
|
|
// importFile imports the file at the given URI, inside the global FILES/ directory.
|
|
// Then calls back the next function, with the downloaded location and the original URI.
|
|
// Callback return is forwarded.
|
|
importFile(URI string, next func(string, string) (interface{}, error)) (interface{}, error)
|
|
// getFileReader returns a reader to the requested file.
|
|
GetFile(filename string) (io.Reader, error)
|
|
// listDir returns a list of the files and subdirectories contained inside the directory at the given location.
|
|
ListDir(filename string) ([]string, error)
|
|
// stat returns many information about the given file: such as last modification date, size, ...
|
|
Stat(filename string) (os.FileInfo, error)
|
|
}
|
|
|
|
// DirectAccessImporter abstracts importer that support direct file access through a local path
|
|
type DirectAccessImporter interface {
|
|
GetLocalPath(p ...string) string
|
|
}
|
|
|
|
// ForgeLinkedImporter abstracts importer that are linked to a forge
|
|
type ForgeLinkedImporter interface {
|
|
GetThemeLink(th *fic.Theme) (*url.URL, error)
|
|
GetExerciceLink(e *fic.Exercice) (*url.URL, error)
|
|
}
|
|
|
|
// WritableImporter abstracts importer that we can also write on
|
|
type WritableImporter interface {
|
|
// writeFile write the given buffer to the file at the given location.
|
|
writeFile(filename string, reader io.Reader) error
|
|
}
|
|
|
|
// GlobalImporter stores the main importer instance to use for global imports.
|
|
var GlobalImporter Importer
|
|
|
|
// GetFileSize returns the size.
|
|
func GetFileSize(i Importer, URI string) (size int64, err error) {
|
|
if i.Exists(URI) {
|
|
if fi, err := i.Stat(URI); err != nil {
|
|
return 0, err
|
|
} else {
|
|
return fi.Size(), nil
|
|
}
|
|
}
|
|
|
|
dirname := path.Dir(URI)
|
|
if i.Exists(dirname) {
|
|
filename := path.Base(URI)
|
|
if files, err := i.ListDir(dirname); err != nil {
|
|
return size, err
|
|
} else {
|
|
for _, fname := range []string{filename, filename + "."} {
|
|
found := false
|
|
for _, file := range files {
|
|
if matched, _ := path.Match(fname+"[0-9][0-9]", file); matched {
|
|
found = true
|
|
if fi, err := i.Stat(path.Join(dirname, file)); err != nil {
|
|
return size, err
|
|
} else {
|
|
size += fi.Size()
|
|
}
|
|
}
|
|
}
|
|
|
|
if found {
|
|
return size, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return size, fmt.Errorf("%q: no such file or directory", URI)
|
|
}
|
|
|
|
// GetFile helps to manage huge file transfert by concatenating splitted (with split(1)) files.
|
|
func GetFile(i Importer, URI string) (io.Reader, func(), error) {
|
|
// Import file if it exists
|
|
if i.Exists(URI) {
|
|
fd, err := i.GetFile(URI)
|
|
return fd, func() {
|
|
if fdc, ok := fd.(io.ReadCloser); ok {
|
|
fdc.Close()
|
|
}
|
|
}, err
|
|
}
|
|
|
|
// Try to find file parts
|
|
dirname := path.Dir(URI)
|
|
if i.Exists(dirname) {
|
|
filename := path.Base(URI)
|
|
if files, err := i.ListDir(dirname); err != nil {
|
|
return nil, nil, err
|
|
} else {
|
|
var readers []io.Reader
|
|
|
|
for _, fname := range []string{filename, filename + "."} {
|
|
for _, file := range files {
|
|
if matched, _ := path.Match(fname+"[0-9][0-9]", file); matched {
|
|
fd, err := i.GetFile(path.Join(dirname, file))
|
|
if err != nil {
|
|
// Close already opened files to avoid leaks
|
|
for _, rd := range readers {
|
|
if rdc, ok := rd.(io.ReadCloser); ok {
|
|
rdc.Close()
|
|
}
|
|
}
|
|
|
|
return nil, nil, err
|
|
}
|
|
|
|
readers = append(readers, fd)
|
|
}
|
|
}
|
|
|
|
if len(readers) > 0 {
|
|
return io.MultiReader(readers...), func() {
|
|
for _, rd := range readers {
|
|
if rdc, ok := rd.(io.ReadCloser); ok {
|
|
rdc.Close()
|
|
}
|
|
}
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, nil, fmt.Errorf("%q: no such file or directory", URI)
|
|
}
|
|
|
|
// GetFileContent retrieves the content of the given text file.
|
|
func GetFileContent(i Importer, URI string) (string, error) {
|
|
if fd, closer, err := GetFile(i, URI); err != nil {
|
|
return "", err
|
|
} else {
|
|
defer closer()
|
|
|
|
buffd := bufio.NewReader(fd)
|
|
|
|
// Ensure we read UTF-8 content.
|
|
buf := make([]rune, 0)
|
|
for b, _, err := buffd.ReadRune(); err == nil; b, _, err = buffd.ReadRune() {
|
|
buf = append(buf, b)
|
|
}
|
|
|
|
if len(buf) == 0 {
|
|
return "", fmt.Errorf("File is empty")
|
|
}
|
|
|
|
return strings.TrimSpace(string(buf)), nil
|
|
}
|
|
}
|
|
|
|
// GetDestinationFilePath generates the destination path, from the URI.
|
|
// This function permits to obfusce to player the original URI.
|
|
// Theoricaly, changing the import method doesn't change destination URI.
|
|
func GetDestinationFilePath(URI string, filename *string) string {
|
|
if filename == nil {
|
|
tmp := path.Base(URI)
|
|
filename = &tmp
|
|
}
|
|
hash := blake2b.Sum512([]byte(URI))
|
|
return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), *filename)
|
|
}
|
|
|
|
func importFile(i Importer, URI string, dest string) error {
|
|
if err := os.MkdirAll(path.Dir(dest), 0751); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write file
|
|
if fdto, err := os.Create(dest); err != nil {
|
|
return err
|
|
} else {
|
|
defer fdto.Close()
|
|
|
|
if fdfrom, closer, err := GetFile(i, URI); err != nil {
|
|
os.Remove(dest)
|
|
return err
|
|
} else {
|
|
defer closer()
|
|
|
|
_, err = io.Copy(fdto, fdfrom)
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// ImportFile imports the file at the given URI, using helpers of the given Importer.
|
|
// After import, next is called with relative path where the file has been saved and the original URI.
|
|
func ImportFile(i Importer, URI string, next func(string, string) (interface{}, error)) (interface{}, error) {
|
|
dest := GetDestinationFilePath(URI, nil)
|
|
|
|
// If the present file is still valide, don't erase it
|
|
if _, err := os.Stat(dest); !os.IsNotExist(err) {
|
|
if r, err := next(dest, URI); err == nil {
|
|
return r, err
|
|
}
|
|
}
|
|
|
|
if err := importFile(i, URI, dest); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return next(dest, URI)
|
|
}
|
|
|
|
// WriteFileContent save the given content to the given text file.
|
|
func WriteFileContent(i Importer, URI string, content []byte) error {
|
|
if wi, ok := i.(WritableImporter); ok {
|
|
return wi.writeFile(URI, bytes.NewReader(content))
|
|
} else {
|
|
return fmt.Errorf("%t is not capable of writing", i)
|
|
}
|
|
}
|