github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/attachments/utils.go (about)

     1  package attachments
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"image/gif"
     8  	"image/png"
     9  	"io"
    10  	"os"
    11  	"strings"
    12  	"sync/atomic"
    13  	"time"
    14  
    15  	"github.com/keybase/client/go/chat/globals"
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/protocol/chat1"
    18  	"github.com/keybase/client/go/protocol/gregor1"
    19  	"github.com/keybase/client/go/protocol/keybase1"
    20  	"github.com/keybase/go-framed-msgpack-rpc/rpc"
    21  	"golang.org/x/net/context"
    22  )
    23  
    24  func AssetFromMessage(ctx context.Context, g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
    25  	msgID chat1.MessageID, preview bool) (res chat1.Asset, err error) {
    26  	reason := chat1.GetThreadReason_GENERAL
    27  	msgs, err := g.ChatHelper.GetMessages(ctx, uid, convID, []chat1.MessageID{msgID}, true, &reason)
    28  	if err != nil {
    29  		return res, err
    30  	}
    31  	if len(msgs) == 0 {
    32  		return res, libkb.NotFoundError{}
    33  	}
    34  	first := msgs[0]
    35  	st, err := first.State()
    36  	if err != nil {
    37  		return res, err
    38  	}
    39  	if st == chat1.MessageUnboxedState_ERROR {
    40  		em := first.Error().ErrMsg
    41  		return res, errors.New(em)
    42  	}
    43  	if st != chat1.MessageUnboxedState_VALID {
    44  		// given a message id that doesn't exist, msgs can come back
    45  		// with an empty message in it (and st == 0).
    46  		// this check prevents a panic, but perhaps the server needs
    47  		// a fix as well.
    48  		return res, libkb.NotFoundError{}
    49  	}
    50  
    51  	msg := first.Valid()
    52  	body := msg.MessageBody
    53  	t, err := body.MessageType()
    54  	if err != nil {
    55  		return res, err
    56  	}
    57  
    58  	var attachment chat1.MessageAttachment
    59  	switch t {
    60  	case chat1.MessageType_ATTACHMENT:
    61  		attachment = msg.MessageBody.Attachment()
    62  	case chat1.MessageType_ATTACHMENTUPLOADED:
    63  		uploaded := msg.MessageBody.Attachmentuploaded()
    64  		attachment = chat1.MessageAttachment{
    65  			Object:   uploaded.Object,
    66  			Previews: uploaded.Previews,
    67  			Metadata: uploaded.Metadata,
    68  		}
    69  	default:
    70  		return res, errors.New("not an attachment message")
    71  	}
    72  	res = attachment.Object
    73  	if preview {
    74  		if len(attachment.Previews) > 0 {
    75  			res = attachment.Previews[0]
    76  		} else if attachment.Preview != nil {
    77  			res = *attachment.Preview
    78  		} else {
    79  			return res, errors.New("no preview in attachment")
    80  		}
    81  	}
    82  	return res, nil
    83  }
    84  
    85  // ReadCloseResetter is io.ReadCloser plus a Reset method. This is used by
    86  // attachment uploads.
    87  type ReadCloseResetter interface {
    88  	io.ReadCloser
    89  	Reset() error
    90  }
    91  
    92  type FileReadCloseResetter struct {
    93  	filename string
    94  	file     *os.File
    95  	buf      *bufio.Reader
    96  }
    97  
    98  // NewFileReadCloseResetter creates a ReadCloseResetter that uses an on-disk file as
    99  // source of data.
   100  func NewFileReadCloseResetter(name string) (ReadCloseResetter, error) {
   101  	f := &FileReadCloseResetter{filename: name}
   102  	if err := f.open(); err != nil {
   103  		return nil, err
   104  	}
   105  	return f, nil
   106  }
   107  
   108  func (f *FileReadCloseResetter) open() error {
   109  	ff, err := os.Open(f.filename)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	f.file = ff
   114  	f.buf = bufio.NewReader(f.file)
   115  	return nil
   116  }
   117  
   118  func (f *FileReadCloseResetter) Read(p []byte) (int, error) {
   119  	return f.buf.Read(p)
   120  }
   121  
   122  func (f *FileReadCloseResetter) Reset() error {
   123  	_, err := f.file.Seek(0, io.SeekStart)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	f.buf.Reset(f.file)
   128  	return nil
   129  }
   130  
   131  func (f *FileReadCloseResetter) Close() error {
   132  	f.buf = nil
   133  	if f.file != nil {
   134  		return f.file.Close()
   135  	}
   136  	return nil
   137  }
   138  
   139  // KbfsReadCloseResetter is an implementation of ReadCloseResetter that uses
   140  // SimpleFS as source of data.
   141  type KbfsReadCloseResetter struct {
   142  	client *keybase1.SimpleFSClient
   143  	opid   keybase1.OpID
   144  	offset int64
   145  	ctx    context.Context
   146  }
   147  
   148  const (
   149  	kbfsPrefix        = "/keybase"
   150  	kbfsPrefixPrivate = kbfsPrefix + "/private/"
   151  	kbfsPrefixPublic  = kbfsPrefix + "/public/"
   152  	kbfsPrefixTeam    = kbfsPrefix + "/team/"
   153  )
   154  
   155  func isKbfsPath(p string) bool {
   156  	return strings.HasPrefix(p, kbfsPrefixPrivate) ||
   157  		strings.HasPrefix(p, kbfsPrefixPublic) ||
   158  		strings.HasPrefix(p, kbfsPrefixTeam)
   159  }
   160  
   161  func makeSimpleFSClientFromGlobalContext(
   162  	g *libkb.GlobalContext) (*keybase1.SimpleFSClient, error) {
   163  	xp := g.ConnectionManager.LookupByClientType(keybase1.ClientType_KBFS)
   164  	if xp == nil {
   165  		return nil, libkb.KBFSNotRunningError{}
   166  	}
   167  	return &keybase1.SimpleFSClient{
   168  		Cli: rpc.NewClient(xp, libkb.NewContextifiedErrorUnwrapper(g), nil),
   169  	}, nil
   170  }
   171  
   172  // NewKbfsReadCloseResetter creates a ReadCloseResetter that uses SimpleFS as source
   173  // of data. kbfsPath must start with "/keybase/<tlf-type>/".
   174  func NewKbfsReadCloseResetter(ctx context.Context, g *libkb.GlobalContext,
   175  	kbfsPath string) (ReadCloseResetter, error) {
   176  	if !isKbfsPath(kbfsPath) {
   177  		return nil, errors.New("not a kbfs path")
   178  	}
   179  
   180  	client, err := makeSimpleFSClientFromGlobalContext(g)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	opid, err := client.SimpleFSMakeOpid(ctx)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	if err = client.SimpleFSOpen(ctx, keybase1.SimpleFSOpenArg{
   191  		OpID:  opid,
   192  		Dest:  keybase1.NewPathWithKbfsPath(kbfsPath[len(kbfsPrefix):]),
   193  		Flags: keybase1.OpenFlags_READ | keybase1.OpenFlags_EXISTING,
   194  	}); err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	return &KbfsReadCloseResetter{
   199  		client: client,
   200  		ctx:    ctx,
   201  		opid:   opid,
   202  	}, nil
   203  }
   204  
   205  // Read implements the ReadCloseResetter interface.
   206  func (f *KbfsReadCloseResetter) Read(p []byte) (int, error) {
   207  	content, err := f.client.SimpleFSRead(f.ctx, keybase1.SimpleFSReadArg{
   208  		OpID:   f.opid,
   209  		Offset: atomic.LoadInt64(&f.offset),
   210  		Size:   len(p),
   211  	})
   212  	if err != nil {
   213  		return 0, err
   214  	}
   215  	if len(content.Data) == 0 {
   216  		// Unfortunately SimpleFSRead doesn't return EOF error.
   217  		return 0, io.EOF
   218  	}
   219  	atomic.AddInt64(&f.offset, int64(len(content.Data)))
   220  	copy(p, content.Data)
   221  	return len(content.Data), nil
   222  }
   223  
   224  // Reset implements the ReadCloseResetter interface.
   225  func (f *KbfsReadCloseResetter) Reset() error {
   226  	atomic.StoreInt64(&f.offset, 0)
   227  	return nil
   228  }
   229  
   230  // Close implements the ReadCloseResetter interface.
   231  func (f *KbfsReadCloseResetter) Close() error {
   232  	return f.client.SimpleFSClose(f.ctx, f.opid)
   233  }
   234  
   235  // NewReadCloseResetter creates a ReadCloseResetter using either on-disk file
   236  // or SimpleFS depending on if p is a KBFS path.
   237  func NewReadCloseResetter(ctx context.Context, g *libkb.GlobalContext,
   238  	p string) (ReadCloseResetter, error) {
   239  	if isKbfsPath(p) {
   240  		return NewKbfsReadCloseResetter(ctx, g, p)
   241  	}
   242  	return NewFileReadCloseResetter(p)
   243  }
   244  
   245  type kbfsFileInfo struct {
   246  	dirent *keybase1.Dirent
   247  }
   248  
   249  func (fi kbfsFileInfo) Name() string { return fi.dirent.Name }
   250  func (fi kbfsFileInfo) Size() int64  { return int64(fi.dirent.Size) }
   251  func (fi kbfsFileInfo) Mode() (mode os.FileMode) {
   252  	mode |= 0400
   253  	if fi.dirent.Writable {
   254  		mode |= 0200
   255  	}
   256  	switch fi.dirent.DirentType {
   257  	case keybase1.DirentType_DIR:
   258  		mode |= os.ModeDir
   259  	case keybase1.DirentType_SYM:
   260  		mode |= os.ModeSymlink
   261  	case keybase1.DirentType_EXEC:
   262  		mode |= 0100
   263  	}
   264  	return mode
   265  }
   266  func (fi kbfsFileInfo) ModTime() time.Time {
   267  	return keybase1.FromTime(fi.dirent.Time)
   268  }
   269  func (fi kbfsFileInfo) IsDir() bool {
   270  	return fi.dirent.DirentType == keybase1.DirentType_DIR
   271  }
   272  func (fi kbfsFileInfo) Sys() interface{} { return fi.dirent }
   273  
   274  // StatOSOrKbfsFile stats the file located at p, using SimpleFSStat if it's a
   275  // KBFS path, or os.Stat if not.
   276  func StatOSOrKbfsFile(ctx context.Context, g *libkb.GlobalContext, p string) (
   277  	fi os.FileInfo, err error) {
   278  	if !isKbfsPath(p) {
   279  		return os.Stat(p)
   280  	}
   281  
   282  	client, err := makeSimpleFSClientFromGlobalContext(g)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  
   287  	dirent, err := client.SimpleFSStat(ctx, keybase1.SimpleFSStatArg{
   288  		Path: keybase1.NewPathWithKbfsPath(p[len(kbfsPrefix):]),
   289  	})
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  
   294  	return kbfsFileInfo{dirent: &dirent}, nil
   295  }
   296  
   297  type BufReadResetter struct {
   298  	buf []byte
   299  	r   *bytes.Reader
   300  }
   301  
   302  func NewBufReadResetter(buf []byte) *BufReadResetter {
   303  	return &BufReadResetter{
   304  		buf: buf,
   305  		r:   bytes.NewReader(buf),
   306  	}
   307  }
   308  
   309  func (b *BufReadResetter) Read(p []byte) (int, error) {
   310  	return b.r.Read(p)
   311  }
   312  
   313  func (b *BufReadResetter) Reset() error {
   314  	b.r.Reset(b.buf)
   315  	return nil
   316  }
   317  
   318  func (b *BufReadResetter) Close() error {
   319  	return nil
   320  }
   321  
   322  func AddPendingPreview(ctx context.Context, g *globals.Context, obr *chat1.OutboxRecord) error {
   323  	pre, err := NewPendingPreviews(g).Get(ctx, obr.OutboxID)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	mpr, err := pre.Export(func() *chat1.PreviewLocation {
   328  		loc := chat1.NewPreviewLocationWithUrl(g.AttachmentURLSrv.GetPendingPreviewURL(ctx,
   329  			obr.OutboxID))
   330  		return &loc
   331  	})
   332  	if err != nil {
   333  		return err
   334  	}
   335  	obr.Preview = &mpr
   336  	return nil
   337  }
   338  
   339  func GIFToPNG(ctx context.Context, src io.Reader, dest io.Writer) error {
   340  	g, err := gif.DecodeAll(src)
   341  	if err != nil {
   342  		return err
   343  	}
   344  	if len(g.Image) == 0 {
   345  		return errors.New("no frames in gif")
   346  	}
   347  	return png.Encode(dest, g.Image[0])
   348  }