Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unfurling: store favicon and image as Asset on Unfurl and upload to S3 CORE-9302 #14540

Merged
merged 34 commits into from Nov 1, 2018
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 9 additions & 13 deletions go/chat/attachments/preprocess.go
Expand Up @@ -10,7 +10,7 @@ import (
"path/filepath"
"strings"

"github.com/keybase/client/go/chat/globals"
"github.com/keybase/client/go/chat/types"
"github.com/keybase/client/go/chat/utils"

"github.com/keybase/client/go/protocol/chat1"
Expand Down Expand Up @@ -181,29 +181,29 @@ func processCallerPreview(ctx context.Context, callerPreview chat1.MakePreviewRe
return p, nil
}

func DetectMIMEType(ctx context.Context, src *os.File) (res string, err error) {
func DetectMIMEType(ctx context.Context, src ReadResetter, filename string) (res string, err error) {
head := make([]byte, 512)
_, err = io.ReadFull(src, head)
if err != nil && err != io.ErrUnexpectedEOF {
return res, err
}

res = http.DetectContentType(head)
if _, err = src.Seek(0, 0); err != nil {
if err = src.Reset(); err != nil {
return res, err
}
// MIME type detection failed us, try using an extension map
if res == "application/octet-stream" {
ext := strings.ToLower(filepath.Ext(src.Name()))
ext := strings.ToLower(filepath.Ext(filename))
if typ, ok := mimeTypes[ext]; ok {
res = typ
}
}
return res, nil
}

func PreprocessAsset(ctx context.Context, g *globals.Context, log utils.DebugLabeler, filename string,
callerPreview *chat1.MakePreviewRes) (p Preprocess, err error) {
func PreprocessAsset(ctx context.Context, log utils.DebugLabeler, src ReadResetter, filename string,
nvh types.NativeVideoHelper, callerPreview *chat1.MakePreviewRes) (p Preprocess, err error) {
if callerPreview != nil && callerPreview.Location != nil {
log.Debug(ctx, "preprocessAsset: caller provided preview, using that")
if p, err = processCallerPreview(ctx, *callerPreview); err != nil {
Expand All @@ -212,17 +212,13 @@ func PreprocessAsset(ctx context.Context, g *globals.Context, log utils.DebugLab
return p, nil
}
}
src, err := os.Open(filename)
if err != nil {
return p, err
}
defer src.Close()
defer src.Reset()

if p.ContentType, err = DetectMIMEType(ctx, src); err != nil {
if p.ContentType, err = DetectMIMEType(ctx, src, filename); err != nil {
return p, err
}
log.Debug(ctx, "preprocessAsset: detected attachment content type %s", p.ContentType)
previewRes, err := Preview(ctx, g, log, src, p.ContentType, filename)
previewRes, err := Preview(ctx, log, src, p.ContentType, filename, nvh)
if err != nil {
log.Debug(ctx, "preprocessAsset: error making preview: %s", err)
return p, err
Expand Down
12 changes: 6 additions & 6 deletions go/chat/attachments/preview.go
Expand Up @@ -14,7 +14,7 @@ import (
"io/ioutil"
"strings"

"github.com/keybase/client/go/chat/globals"
"github.com/keybase/client/go/chat/types"
"github.com/keybase/client/go/chat/utils"

"golang.org/x/net/context"
Expand Down Expand Up @@ -42,30 +42,30 @@ type PreviewRes struct {

// Preview creates preview assets from src. It returns an in-memory BufferSource
// and the content type of the preview asset.
func Preview(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src io.Reader, contentType,
basename string) (*PreviewRes, error) {
func Preview(ctx context.Context, log utils.DebugLabeler, src ReadResetter, contentType,
basename string, nvh types.NativeVideoHelper) (*PreviewRes, error) {
switch contentType {
case "image/jpeg", "image/png":
return previewImage(ctx, log, src, basename, contentType)
case "image/gif":
return previewGIF(ctx, log, src, basename)
}
if strings.HasPrefix(contentType, "video") {
pre, err := previewVideo(ctx, g, log, src, basename)
pre, err := previewVideo(ctx, log, src, basename, nvh)
if err == nil {
log.Debug(ctx, "Preview: found video preview for filename: %s contentType: %s", basename,
contentType)
return pre, nil
}
log.Debug(ctx, "Preview: failed to get video preview for filename: %s contentType: %s err: %s",
basename, contentType, err)
return previewVideoBlank(ctx, g, log, src, basename)
return previewVideoBlank(ctx, log, src, basename)
}
return nil, nil
}

// previewVideoBlank previews a video by inserting a black rectangle with a play button on it.
func previewVideoBlank(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src io.Reader,
func previewVideoBlank(ctx context.Context, log utils.DebugLabeler, src io.Reader,
basename string) (res *PreviewRes, err error) {
const width, height = 300, 150
img := image.NewNRGBA(image.Rect(0, 0, width, height))
Expand Down
10 changes: 5 additions & 5 deletions go/chat/attachments/preview_android.go
Expand Up @@ -6,22 +6,22 @@ import (
"bytes"
"io"

"github.com/keybase/client/go/chat/globals"
"github.com/keybase/client/go/chat/types"
"github.com/keybase/client/go/chat/utils"
"golang.org/x/net/context"
)

func previewVideo(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src io.Reader,
basename string) (res *PreviewRes, err error) {
func previewVideo(ctx context.Context, log utils.DebugLabeler, src io.Reader,
basename string, nvh types.NativeVideoHelper) (res *PreviewRes, err error) {
defer log.Trace(ctx, func() error { return err }, "previewVideo")()
dat, duration, err := g.NativeVideoHelper.ThumbnailAndDuration(ctx, basename)
dat, duration, err := nvh.ThumbnailAndDuration(ctx, basename)
if err != nil {
return res, err
}
log.Debug(ctx, "previewVideo: size: %d duration: %d", len(dat), duration)
if len(dat) == 0 {
log.Debug(ctx, "failed to generate preview from native, using blank image")
return previewVideoBlank(ctx, g, log, src, basename)
return previewVideoBlank(ctx, log, src, basename)
}
imagePreview, err := previewImage(ctx, log, bytes.NewReader(dat), basename, "image/jpeg")
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions go/chat/attachments/preview_darwin.go
Expand Up @@ -69,13 +69,13 @@ import (
"io"
"unsafe"

"github.com/keybase/client/go/chat/globals"
"github.com/keybase/client/go/chat/types"
"github.com/keybase/client/go/chat/utils"
"golang.org/x/net/context"
)

func previewVideo(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src io.Reader,
basename string) (res *PreviewRes, err error) {
func previewVideo(ctx context.Context, log utils.DebugLabeler, src io.Reader,
basename string, nvh types.NativeVideoHelper) (res *PreviewRes, err error) {
defer log.Trace(ctx, func() error { return err }, "previewVideo")()
C.MakeVideoThumbnail(C.CString(basename))
duration := int(C.VideoDuration())
Expand Down
8 changes: 4 additions & 4 deletions go/chat/attachments/preview_dummy.go
Expand Up @@ -5,12 +5,12 @@ package attachments
import (
"io"

"github.com/keybase/client/go/chat/globals"
"github.com/keybase/client/go/chat/types"
"github.com/keybase/client/go/chat/utils"
"golang.org/x/net/context"
)

func previewVideo(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src io.Reader,
basename string) (*PreviewRes, error) {
return previewVideoBlank(ctx, g, log, src, basename)
func previewVideo(ctx context.Context, log utils.DebugLabeler, src io.Reader,
basename string, nvh types.NativeVideoHelper) (*PreviewRes, error) {
return previewVideoBlank(ctx, log, src, basename)
}
7 changes: 6 additions & 1 deletion go/chat/attachments/sender.go
Expand Up @@ -27,7 +27,12 @@ func NewSender(gc *globals.Context) *Sender {

func (s *Sender) MakePreview(ctx context.Context, filename string, outboxID chat1.OutboxID) (res chat1.MakePreviewRes, err error) {
defer s.Trace(ctx, func() error { return err }, "MakePreview")()
pre, err := PreprocessAsset(ctx, s.G(), s.DebugLabeler, filename, nil)
src, err := newFileReadResetter(filename)
if err != nil {
return res, err
}
defer src.Close()
pre, err := PreprocessAsset(ctx, s.DebugLabeler, src, filename, s.G().NativeVideoHelper, nil)
if err != nil {
return chat1.MakePreviewRes{}, err
}
Expand Down
4 changes: 2 additions & 2 deletions go/chat/attachments/uploader.go
Expand Up @@ -405,7 +405,7 @@ func (u *Uploader) upload(ctx context.Context, uid gregor1.UID, convID chat1.Con
// preprocess asset (get content type, create preview if possible)
var ures types.AttachmentUploadResult
ures.Metadata = metadata
pre, err := PreprocessAsset(ctx, u.G(), u.DebugLabeler, filename, callerPreview)
pre, err := PreprocessAsset(ctx, u.DebugLabeler, src, filename, u.G().NativeVideoHelper, callerPreview)
if err != nil {
return res, err
}
Expand Down Expand Up @@ -507,7 +507,7 @@ func (u *Uploader) upload(ctx context.Context, uid gregor1.UID, convID chat1.Con
S3Params: previewParams,
Filename: filename,
FileSize: int64(len(pre.Preview)),
Plaintext: newBufReadResetter(pre.Preview),
Plaintext: NewBufReadResetter(pre.Preview),
S3Signer: u.s3signer,
ConversationID: convID,
UserID: uid,
Expand Down
10 changes: 5 additions & 5 deletions go/chat/attachments/utils.go
Expand Up @@ -120,23 +120,23 @@ func (f *fileReadResetter) Close() error {
return nil
}

type bufReadResetter struct {
type BufReadResetter struct {
buf []byte
r *bytes.Reader
}

func newBufReadResetter(buf []byte) *bufReadResetter {
return &bufReadResetter{
func NewBufReadResetter(buf []byte) *BufReadResetter {
return &BufReadResetter{
buf: buf,
r: bytes.NewReader(buf),
}
}

func (b *bufReadResetter) Read(p []byte) (int, error) {
func (b *BufReadResetter) Read(p []byte) (int, error) {
return b.r.Read(p)
}

func (b *bufReadResetter) Reset() error {
func (b *BufReadResetter) Reset() error {
b.r.Reset(b.buf)
return nil
}
133 changes: 133 additions & 0 deletions go/chat/unfurl/packager.go
@@ -0,0 +1,133 @@
package unfurl

import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/keybase/client/go/chat/attachments"
"github.com/keybase/client/go/chat/s3"
"github.com/keybase/client/go/chat/storage"

"github.com/keybase/client/go/chat/utils"
"github.com/keybase/client/go/logger"
"github.com/keybase/client/go/protocol/chat1"
"github.com/keybase/client/go/protocol/gregor1"
)

type Packager struct {
utils.DebugLabeler

ri func() chat1.RemoteInterface
store attachments.Store
s3signer s3.Signer
maxAssetSize int
}

func NewPackager(l logger.Logger, store attachments.Store, s3signer s3.Signer,
ri func() chat1.RemoteInterface) *Packager {
return &Packager{
DebugLabeler: utils.NewDebugLabeler(l, "Packager", false),
store: store,
ri: ri,
s3signer: s3signer,
maxAssetSize: 2000000,
}
}

func (p *Packager) assetFilename(url string) string {
toks := strings.Split(url, "/")
if len(toks) > 0 {
return toks[len(toks)-1]
}
return "unknown.jpg"
joshblum marked this conversation as resolved.
Show resolved Hide resolved
}

func (p *Packager) assetFromURL(ctx context.Context, url string, uid gregor1.UID,
convID chat1.ConversationID) (res chat1.Asset, err error) {
resp, err := http.Get(url)
if err != nil {
return res, err
}
defer resp.Body.Close()
dat, err := ioutil.ReadAll(resp.Body)
mmaxim marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return res, err
}
if len(dat) > p.maxAssetSize {
return res, fmt.Errorf("asset too large: %d > %d", len(dat), p.maxAssetSize)
}
filename := p.assetFilename(url)
src := attachments.NewBufReadResetter(dat)
pre, err := attachments.PreprocessAsset(ctx, p.DebugLabeler, src, filename, nil)
if err != nil {
return res, err
}
if pre.Preview == nil {
return res, errors.New("unable to get preview from URL asset")
}

s3params, err := p.ri().GetS3Params(ctx, convID)
if err != nil {
return res, err
}
outboxID, err := storage.NewOutboxID()
if err != nil {
return res, err
}
task := attachments.UploadTask{
S3Params: s3params,
Filename: filename,
FileSize: int64(len(dat)),
Plaintext: attachments.NewBufReadResetter(pre.Preview),
S3Signer: p.s3signer,
ConversationID: convID,
UserID: uid,
OutboxID: outboxID,
}
if res, err = p.store.UploadAsset(ctx, &task, ioutil.Discard); err != nil {
return res, err
}
res.MimeType = pre.PreviewContentType
res.Metadata = pre.PreviewMetadata()
return res, nil
}

func (p *Packager) Package(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) {
defer p.Trace(ctx, func() error { return err }, "Package")()
typ, err := raw.UnfurlType()
if err != nil {
return res, err
}
switch typ {
case chat1.UnfurlType_GENERIC:
g := chat1.UnfurlGeneric{
Title: raw.Generic().Title,
Url: raw.Generic().Url,
SiteName: raw.Generic().SiteName,
PublishTime: raw.Generic().PublishTime,
Description: raw.Generic().Description,
}
if raw.Generic().ImageUrl != nil {
asset, err := p.assetFromURL(ctx, *raw.Generic().ImageUrl, uid, convID)
if err != nil {
return res, err
}
g.Image = &asset
}
if raw.Generic().FaviconUrl != nil {
asset, err := p.assetFromURL(ctx, *raw.Generic().FaviconUrl, uid, convID)
if err != nil {
return res, err
}
g.Favicon = &asset
}
return chat1.NewUnfurlWithGeneric(g), nil
default:
return res, errors.New("not implemented")
}
}