diff options
Diffstat (limited to 'internal/hole/hole.go')
| -rw-r--r-- | internal/hole/hole.go | 278 |
1 files changed, 278 insertions, 0 deletions
diff --git a/internal/hole/hole.go b/internal/hole/hole.go new file mode 100644 index 0000000..3749f9b --- /dev/null +++ b/internal/hole/hole.go @@ -0,0 +1,278 @@ +// Copyright 2026 Rose Hogenson <r@roseh.moe> +// +// This file is part of roseh.moe. +// +// roseh.moe is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// roseh.moe is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty +// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with roseh.moe. If not, see +// <https://www.gnu.org/licenses/>. + +package hole + +import ( + "archive/tar" + "cmp" + "compress/gzip" + "crypto/hkdf" + "crypto/rand" + "crypto/sha256" + "encoding/binary" + "errors" + "fmt" + "io" + "io/fs" + "log" + "mime/multipart" + "os" + "strings" + "time" + + "roseh.moe/pkg/oae2" + "roseh.moe/pkg/wordlist" +) + +var HoleTempDir string + +func makeHole() string { + const nWords = 10 + buf := make([]byte, 2*nWords) + rand.Read(buf) + words := make([]string, nWords) + for i := range words { + words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff] + } + return strings.Join(words, "-") +} + +const blockSize = 4 * 1024 * 1024 + +type holeFileWriter struct { + *oae2.Writer + f *os.File +} + +func (w *holeFileWriter) Close() error { + return cmp.Or(w.Writer.Close(), w.f.Close()) +} + +func writeHoleFile(filename string) (_ string, _ *holeFileWriter, err error) { + hole := makeHole() + primaryKey, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 32) + if err != nil { + return "", nil, err + } + f, err := os.Create(fmt.Sprintf("%s/%x", HoleTempDir, primaryKey)) + if err != nil { + return "", nil, err + } + defer func() { + if err != nil { + f.Close() + } + }() + w := oae2.NewWriter(f, []byte(hole), blockSize, nil) + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(len(filename))) + if _, err := w.Write(buf); err != nil { + return "", nil, err + } + if _, err := io.WriteString(w, filename); err != nil { + return "", nil, err + } + return hole, &holeFileWriter{w, f}, nil +} + +type offsetReader struct { + io.ReadSeeker + offset int +} + +func (r *offsetReader) Seek(offset int64, whence int) (int64, error) { + if whence == io.SeekStart { + offset += int64(r.offset) + } + n, err := r.ReadSeeker.Seek(offset, whence) + return n - int64(r.offset), err +} + +type HoleFileReader struct { + *offsetReader + F *os.File +} + +func (r *HoleFileReader) Close() error { + return r.F.Close() +} + +var ErrNoHole = errors.New("no such hole") + +func Open(hole string) (_ string, _ *HoleFileReader, err error) { + primaryKey, err := hkdf.Key(sha256.New, []byte(hole), nil, "", 32) + if err != nil { + return "", nil, err + } + f, err := os.Open(fmt.Sprintf("%s/%x", HoleTempDir, primaryKey)) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return "", nil, ErrNoHole + } + return "", nil, err + } + defer func() { + if err != nil { + f.Close() + } + }() + r := oae2.NewReader(f, []byte(hole), blockSize, nil) + buf := make([]byte, 8) + if _, err := io.ReadFull(r, buf); err != nil { + return "", nil, err + } + fileNameLen := binary.BigEndian.Uint64(buf) + fileName := make([]byte, fileNameLen) + if _, err := io.ReadFull(r, fileName); err != nil { + return "", nil, err + } + return string(fileName), &HoleFileReader{&offsetReader{r, 8 + int(fileNameLen)}, f}, nil +} + +func writeHoleFileToTarFile(tarWriter *tar.Writer, hole string, now time.Time) error { + fileName, f, err := Open(hole) + if err != nil { + return fmt.Errorf("read back hole file: %s", err) + } + defer f.Close() + size, err := f.Seek(0, io.SeekEnd) + if err != nil { + return fmt.Errorf("read back hole file: %s", err) + } + if _, err := f.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("read back hole file: %s", err) + } + if err := tarWriter.WriteHeader(&tar.Header{Name: "wormhole-files/" + fileName, Mode: 0644, Size: size, ModTime: now, Format: tar.FormatPAX}); err != nil { + return err + } + if _, err := io.Copy(tarWriter, f); err != nil { + return fmt.Errorf("write file to tar file: %s", err) + } + return nil +} + +var ErrNoFile = errors.New("no file") + +func Upload(reader *multipart.Reader) (string, error) { + var holes []string + for { + part, err := reader.NextPart() + if err != nil { + break + } + if part.FormName() != "file" || part.FileName() == "" { + continue + } + hole, err := WriteSingleFile(part.FileName(), part) + if err != nil { + return "", err + } + holes = append(holes, hole) + } + if len(holes) == 0 { + return "", ErrNoFile + } + if len(holes) == 1 { + return holes[0], nil + } + tarFileHole, tarHoleWriter, err := writeHoleFile("wormhole-files.tar.gz") + if err != nil { + return "", fmt.Errorf("make tar file: %s", err) + } + defer tarHoleWriter.Close() + gzipWriter, err := gzip.NewWriterLevel(tarHoleWriter, gzip.BestSpeed) + if err != nil { + return "", err + } + tarWriter := tar.NewWriter(gzipWriter) + now := time.Now() + if err := tarWriter.WriteHeader(&tar.Header{Name: "wormhole-files/", Mode: 0755, ModTime: now, Format: tar.FormatPAX}); err != nil { + return "", fmt.Errorf("create tar file main directory: %s", err) + } + for _, hole := range holes { + if err := writeHoleFileToTarFile(tarWriter, hole, now); err != nil { + return "", err + } + } + if err := tarWriter.Close(); err != nil { + return "", fmt.Errorf("write tar file: %s", err) + } + if err := gzipWriter.Close(); err != nil { + return "", fmt.Errorf("gzip tar file: %s", err) + } + if err := tarHoleWriter.Close(); err != nil { + return "", fmt.Errorf("write tar hole file: %s", err) + } + return tarFileHole, nil +} + +func WriteSingleFile(filename string, r io.Reader) (string, error) { + hole, holeWriter, err := writeHoleFile(filename) + if err != nil { + return "", fmt.Errorf("write hole file: %s", err) + } + defer holeWriter.Close() + if _, err := io.Copy(holeWriter, r); err != nil { + return "", fmt.Errorf("write hole file: %s", err) + } + if err := holeWriter.Close(); err != nil { + return "", fmt.Errorf("write hole file: %s", err) + } + return hole, nil +} + +func Clean() error { + log.Printf("Cleaning hole...") + holeDir, err := os.Open(HoleTempDir) + if err != nil { + return err + } + defer holeDir.Close() + deadline := time.Now().Add(-time.Hour) + for { + files, err := holeDir.ReadDir(1024) + for _, file := range files { + stat, err := file.Info() + if err != nil { + log.Printf("Error: clean hole: %s", err) + continue + } + if !stat.ModTime().Before(deadline) { + continue + } + if err := os.Remove(HoleTempDir + "/" + file.Name()); err != nil { + log.Printf("Error: clean hole: %s", err) + continue + } + } + if err != nil { + if err != io.EOF { + return err + } + return nil + } + } +} + +func Init() error { + if err := os.MkdirAll(HoleTempDir, 0700); err != nil { + return err + } + return nil +} |
