2023-11-10 19:29:26 +01:00
|
|
|
// GoToSocial
|
|
|
|
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
|
//
|
|
|
|
|
// This program 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.
|
|
|
|
|
//
|
|
|
|
|
// This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
|
|
package media
|
|
|
|
|
|
2024-07-12 09:39:47 +00:00
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
2024-08-30 14:03:59 +02:00
|
|
|
"io/fs"
|
2024-07-12 09:39:47 +00:00
|
|
|
"os"
|
2024-08-30 14:03:59 +02:00
|
|
|
"path"
|
2024-07-12 09:39:47 +00:00
|
|
|
|
2025-04-26 15:34:10 +02:00
|
|
|
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
2024-07-12 09:39:47 +00:00
|
|
|
"codeberg.org/gruf/go-bytesize"
|
|
|
|
|
"codeberg.org/gruf/go-iotools"
|
|
|
|
|
)
|
|
|
|
|
|
2025-09-24 15:12:25 +02:00
|
|
|
// media processing tmpdir.
|
|
|
|
|
var tmpdir = os.TempDir()
|
|
|
|
|
|
2024-08-30 14:03:59 +02:00
|
|
|
// file represents one file
|
|
|
|
|
// with the given flag and perms.
|
|
|
|
|
type file struct {
|
2025-09-24 15:12:25 +02:00
|
|
|
abs string // absolute file path, including root
|
|
|
|
|
dir string // containing directory of abs
|
|
|
|
|
rel string // relative to root, i.e. trim_prefix(abs, dir)
|
2024-08-30 14:03:59 +02:00
|
|
|
flag int
|
|
|
|
|
perm os.FileMode
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-24 15:12:25 +02:00
|
|
|
// allowRead returns a new file{} for filepath permitted only to read.
|
|
|
|
|
func allowRead(filepath string) file {
|
|
|
|
|
return newFile(filepath, os.O_RDONLY, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// allowCreate returns a new file{} for filepath permitted to read / write / create.
|
|
|
|
|
func allowCreate(filepath string) file {
|
|
|
|
|
return newFile(filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newFile returns a new instance of file{} for given path and open args.
|
|
|
|
|
func newFile(filepath string, flag int, perms os.FileMode) file {
|
|
|
|
|
dir, rel := path.Split(filepath)
|
|
|
|
|
return file{
|
|
|
|
|
abs: filepath,
|
|
|
|
|
rel: rel,
|
|
|
|
|
dir: dir,
|
|
|
|
|
flag: flag,
|
|
|
|
|
perm: perms,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-30 14:03:59 +02:00
|
|
|
// allowFiles implements fs.FS to allow
|
|
|
|
|
// access to a specified slice of files.
|
|
|
|
|
type allowFiles []file
|
|
|
|
|
|
|
|
|
|
// Open implements fs.FS.
|
|
|
|
|
func (af allowFiles) Open(name string) (fs.File, error) {
|
|
|
|
|
for _, file := range af {
|
2025-09-24 15:12:25 +02:00
|
|
|
switch name {
|
2024-08-30 14:03:59 +02:00
|
|
|
// Allowed to open file
|
2025-09-24 15:12:25 +02:00
|
|
|
// at absolute path, or
|
|
|
|
|
// relative as ffmpeg likes.
|
|
|
|
|
case file.abs, file.rel:
|
|
|
|
|
return os.OpenFile(file.abs, file.flag, file.perm)
|
|
|
|
|
|
|
|
|
|
// Ffmpeg likes to read containing
|
|
|
|
|
// dir as '.'. Allow RO access here.
|
|
|
|
|
case ".":
|
|
|
|
|
return openRead(file.dir)
|
2024-08-30 14:03:59 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil, os.ErrPermission
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-24 15:12:25 +02:00
|
|
|
// openRead opens the existing file at path for reads only.
|
|
|
|
|
func openRead(path string) (*os.File, error) {
|
|
|
|
|
return os.OpenFile(path, os.O_RDONLY, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// openWrite opens the (new!) file at path for read / writes.
|
|
|
|
|
func openWrite(path string) (*os.File, error) {
|
|
|
|
|
return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-08 17:12:13 +00:00
|
|
|
// getExtension splits file extension from path.
|
|
|
|
|
func getExtension(path string) string {
|
|
|
|
|
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
|
|
|
|
|
if path[i] == '.' {
|
|
|
|
|
return path[i+1:]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-12 09:39:47 +00:00
|
|
|
// drainToTmp drains data from given reader into a new temp file
|
|
|
|
|
// and closes it, returning the path of the resulting temp file.
|
2023-11-10 19:29:26 +01:00
|
|
|
//
|
2024-07-12 09:39:47 +00:00
|
|
|
// Note that this function specifically makes attempts to unwrap the
|
|
|
|
|
// io.ReadCloser as much as it can to underlying type, to maximise
|
|
|
|
|
// chance that Linux's sendfile syscall can be utilised for optimal
|
|
|
|
|
// draining of data source to temporary file storage.
|
|
|
|
|
func drainToTmp(rc io.ReadCloser) (string, error) {
|
2025-09-24 15:12:25 +02:00
|
|
|
var tmp *os.File
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
// Close handles
|
|
|
|
|
// on func return.
|
|
|
|
|
defer func() {
|
|
|
|
|
tmp.Close()
|
|
|
|
|
rc.Close()
|
|
|
|
|
}()
|
2024-08-02 14:11:24 +00:00
|
|
|
|
|
|
|
|
// Open new temporary file.
|
2025-09-24 15:12:25 +02:00
|
|
|
tmp, err = os.CreateTemp(
|
|
|
|
|
tmpdir,
|
2024-08-02 14:11:24 +00:00
|
|
|
"gotosocial-*",
|
|
|
|
|
)
|
2024-07-12 09:39:47 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract file path.
|
|
|
|
|
path := tmp.Name()
|
|
|
|
|
|
|
|
|
|
// Limited reader (if any).
|
|
|
|
|
var lr *io.LimitedReader
|
|
|
|
|
var limit int64
|
|
|
|
|
|
|
|
|
|
// Reader type to use
|
|
|
|
|
// for draining to tmp.
|
|
|
|
|
rd := (io.Reader)(rc)
|
|
|
|
|
|
|
|
|
|
// Check if reader is actually wrapped,
|
|
|
|
|
// (as our http client wraps close func).
|
|
|
|
|
rct, ok := rc.(*iotools.ReadCloserType)
|
|
|
|
|
if ok {
|
|
|
|
|
|
|
|
|
|
// Get unwrapped.
|
|
|
|
|
rd = rct.Reader
|
|
|
|
|
|
|
|
|
|
// Extract limited reader if wrapped.
|
|
|
|
|
lr, limit = iotools.GetReaderLimit(rd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drain reader into tmp.
|
|
|
|
|
_, err = tmp.ReadFrom(rd)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return path, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check to see if limit was reached,
|
|
|
|
|
// (produces more useful error messages).
|
2024-09-27 11:15:53 +00:00
|
|
|
if lr != nil && lr.N <= 0 {
|
2024-10-16 14:13:58 +02:00
|
|
|
err := fmt.Errorf("reached read limit %s", bytesize.Size(limit)) // #nosec G115 -- Just logging
|
2024-09-27 11:15:53 +00:00
|
|
|
return path, gtserror.SetLimitReached(err)
|
2024-07-12 09:39:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remove only removes paths if not-empty.
|
|
|
|
|
func remove(paths ...string) error {
|
|
|
|
|
var errs []error
|
|
|
|
|
for _, path := range paths {
|
|
|
|
|
if path != "" {
|
|
|
|
|
if err := os.Remove(path); err != nil {
|
|
|
|
|
errs = append(errs, fmt.Errorf("error removing %s: %w", path, err))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return errors.Join(errs...)
|
2023-11-10 19:29:26 +01:00
|
|
|
}
|