package sync import ( "bufio" "bytes" "encoding/base32" "fmt" "io" "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) // getFile write to the given buffer, the file at the given location. getFile(filename string, writer *bufio.Writer) 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) } // 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, writer *bufio.Writer) error { // Import file if it exists if i.exists(URI) { return i.getFile(URI, writer) } // 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 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 err := i.getFile(path.Join(dirname, file), writer); err != nil { return err } } } if found { return nil } } } } return 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) { cnt := bytes.Buffer{} if err := getFile(i, URI, bufio.NewWriter(io.Writer(&cnt))); err != nil { return "", err } else { // Ensure we read UTF-8 content. buf := make([]rune, 0) for b, _, err := cnt.ReadRune(); err == nil; b, _, err = cnt.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) string { hash := blake2b.Sum512([]byte(URI)) return path.Join(fic.FilesDir, strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:])), path.Base(URI)) } // 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) // 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 := os.MkdirAll(path.Dir(dest), 0755); err != nil { return nil, err } // Write file if fdto, err := os.Create(dest); err != nil { return nil, err } else { defer fdto.Close() writer := bufio.NewWriter(fdto) if err := getFile(i, URI, writer); err != nil { os.Remove(dest) return nil, err } } return next(dest, URI) }