Full Mattermost server source with integrated Community Enterprise features. Includes vendor directory for offline/air-gapped builds. Structure: - enterprise-impl/: Enterprise feature implementations - enterprise-community/: Init files that register implementations - enterprise/: Bridge imports (community_imports.go) - vendor/: All dependencies for offline builds Build (online): go build ./cmd/mattermost Build (offline/air-gapped): go build -mod=vendor ./cmd/mattermost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
4.5 KiB
Go
179 lines
4.5 KiB
Go
package archives
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nwaples/rardecode/v2"
|
|
)
|
|
|
|
func init() {
|
|
RegisterFormat(Rar{})
|
|
}
|
|
|
|
type rarReader interface {
|
|
Next() (*rardecode.FileHeader, error)
|
|
io.Reader
|
|
}
|
|
|
|
type Rar struct {
|
|
// If true, errors encountered during reading or writing
|
|
// a file within an archive will be logged and the
|
|
// operation will continue on remaining files.
|
|
ContinueOnError bool
|
|
|
|
// Password to open archives.
|
|
Password string
|
|
|
|
// Name for a multi-volume archive. When Name is specified,
|
|
// the named file is extracted (rather than any io.Reader that
|
|
// may be passed to Extract). If the archive is a multi-volume
|
|
// archive, this name will also be used by the decoder to derive
|
|
// the filename of the next volume in the volume set.
|
|
Name string
|
|
|
|
// FS is an fs.FS exposing the files of the archive. Unless Name is
|
|
// also specified, this does nothing. When Name is also specified,
|
|
// FS defines the fs.FS that from which the archive will be opened,
|
|
// and in the case of a multi-volume archive, from where each subsequent
|
|
// volume of the volume set will be loaded.
|
|
//
|
|
// Typically this should be a DirFS pointing at the directory containing
|
|
// the volumes of the archive.
|
|
FS fs.FS
|
|
}
|
|
|
|
func (Rar) Extension() string { return ".rar" }
|
|
func (Rar) MediaType() string { return "application/vnd.rar" }
|
|
|
|
func (r Rar) Match(_ context.Context, filename string, stream io.Reader) (MatchResult, error) {
|
|
var mr MatchResult
|
|
|
|
// match filename
|
|
if strings.Contains(strings.ToLower(filename), r.Extension()) {
|
|
mr.ByName = true
|
|
}
|
|
|
|
// match file header (there are two versions; allocate buffer for larger one)
|
|
buf, err := readAtMost(stream, len(rarHeaderV5_0))
|
|
if err != nil {
|
|
return mr, err
|
|
}
|
|
|
|
matchedV1_5 := len(buf) >= len(rarHeaderV1_5) &&
|
|
bytes.Equal(rarHeaderV1_5, buf[:len(rarHeaderV1_5)])
|
|
matchedV5_0 := len(buf) >= len(rarHeaderV5_0) &&
|
|
bytes.Equal(rarHeaderV5_0, buf[:len(rarHeaderV5_0)])
|
|
|
|
mr.ByStream = matchedV1_5 || matchedV5_0
|
|
|
|
return mr, nil
|
|
}
|
|
|
|
// Archive is not implemented for RAR because it is patent-encumbered.
|
|
|
|
func (r Rar) Extract(ctx context.Context, sourceArchive io.Reader, handleFile FileHandler) error {
|
|
var options []rardecode.Option
|
|
if r.Password != "" {
|
|
options = append(options, rardecode.Password(r.Password))
|
|
}
|
|
|
|
if r.FS != nil {
|
|
options = append(options, rardecode.FileSystem(r.FS))
|
|
}
|
|
|
|
var (
|
|
rr rarReader
|
|
err error
|
|
)
|
|
|
|
// If a name has been provided, then the sourceArchive stream is ignored
|
|
// and the archive is opened directly via the filesystem (or provided FS).
|
|
if r.Name != "" {
|
|
var or *rardecode.ReadCloser
|
|
if or, err = rardecode.OpenReader(r.Name, options...); err == nil {
|
|
rr = or
|
|
defer or.Close()
|
|
}
|
|
} else {
|
|
rr, err = rardecode.NewReader(sourceArchive, options...)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// important to initialize to non-nil, empty value due to how fileIsIncluded works
|
|
skipDirs := skipList{}
|
|
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return err // honor context cancellation
|
|
}
|
|
|
|
hdr, err := rr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
if r.ContinueOnError {
|
|
log.Printf("[ERROR] Advancing to next file in rar archive: %v", err)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
if fileIsIncluded(skipDirs, hdr.Name) {
|
|
continue
|
|
}
|
|
|
|
info := rarFileInfo{hdr}
|
|
file := FileInfo{
|
|
FileInfo: info,
|
|
Header: hdr,
|
|
NameInArchive: hdr.Name,
|
|
Open: func() (fs.File, error) {
|
|
return fileInArchive{io.NopCloser(rr), info}, nil
|
|
},
|
|
}
|
|
|
|
err = handleFile(ctx, file)
|
|
if errors.Is(err, fs.SkipAll) {
|
|
break
|
|
} else if errors.Is(err, fs.SkipDir) && file.IsDir() {
|
|
skipDirs.add(hdr.Name)
|
|
} else if err != nil {
|
|
return fmt.Errorf("handling file: %s: %w", hdr.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// rarFileInfo satisfies the fs.FileInfo interface for RAR entries.
|
|
type rarFileInfo struct {
|
|
fh *rardecode.FileHeader
|
|
}
|
|
|
|
func (rfi rarFileInfo) Name() string { return path.Base(rfi.fh.Name) }
|
|
func (rfi rarFileInfo) Size() int64 { return rfi.fh.UnPackedSize }
|
|
func (rfi rarFileInfo) Mode() os.FileMode { return rfi.fh.Mode() }
|
|
func (rfi rarFileInfo) ModTime() time.Time { return rfi.fh.ModificationTime }
|
|
func (rfi rarFileInfo) IsDir() bool { return rfi.fh.IsDir }
|
|
func (rfi rarFileInfo) Sys() any { return nil }
|
|
|
|
var (
|
|
rarHeaderV1_5 = []byte("Rar!\x1a\x07\x00") // v1.5
|
|
rarHeaderV5_0 = []byte("Rar!\x1a\x07\x01\x00") // v5.0
|
|
)
|
|
|
|
// Interface guard
|
|
var _ Extractor = Rar{}
|