| 
									
										
										
										
											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
										 |  |  | } |