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 }