wechaty-puppet/filebox/file_box.go
package filebox
import (
"bufio"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/tuotoo/qrcode"
"github.com/wechaty/go-wechaty/wechaty-puppet/helper"
logger "github.com/wechaty/go-wechaty/wechaty-puppet/log"
"io"
"io/ioutil"
"mime"
"net/url"
"os"
path2 "path"
"path/filepath"
"strings"
)
var (
// ErrToJSON err to json
ErrToJSON = errors.New("FileBox.toJSON() only support TypeUrl,TypeQRCode,TypeBase64, TypeUuid")
// ErrNoBase64Data no base64 data
ErrNoBase64Data = errors.New("no Base64 data")
// ErrNoUrl no url
ErrNoUrl = errors.New("no url")
// ErrNoPath no path
ErrNoPath = errors.New("no path")
// ErrNoQRCode no QR Code
ErrNoQRCode = errors.New("no QR Code")
// ErrNoUuid no uuid
ErrNoUuid = errors.New("no uuid")
)
var log = logger.L.WithField("module", "filebox")
type fileImplInterface interface {
toJSONMap() (map[string]interface{}, error)
toReader() (io.Reader, error)
}
// FileBox struct
type FileBox struct {
fileImpl fileImplInterface
Name string
metadata map[string]interface{}
boxType Type
mediaType string
size int64
md5 string
err error
}
func newFileBox(boxType Type, fileImpl fileImplInterface, options Options) *FileBox {
fb := &FileBox{
fileImpl: fileImpl,
Name: options.Name,
metadata: options.Metadata,
boxType: boxType,
size: options.Size,
md5: options.Md5,
}
if fb.metadata == nil {
fb.metadata = make(map[string]interface{})
}
fb.correctName()
fb.guessMediaType()
return fb
}
func (fb *FileBox) correctName() {
if strings.HasSuffix(fb.Name, ".silk") || strings.HasSuffix(fb.Name, ".slk") {
log.Warn("detect that you want to send voice file which should be <name>.sil pattern. So we help you rename it.")
if strings.HasSuffix(fb.Name, ".silk") {
fb.Name = strings.ReplaceAll(fb.Name, ".silk", ".sil")
}
if strings.HasSuffix(fb.Name, ".slk") {
fb.Name = strings.ReplaceAll(fb.Name, ".slk", ".sil")
}
}
}
func (fb *FileBox) guessMediaType() {
if strings.HasSuffix(fb.Name, ".sil") {
fb.mediaType = "audio/silk"
if _, ok := fb.metadata["voiceLength"]; !ok {
log.Warn("detect that you want to send voice file, but no voiceLength setting, " +
`so use the default setting: 1000,` +
`you should set it manually: filebox.WithMetadata(map[string]interface{}{"voiceLength": 2000})`)
fb.metadata["voiceLength"] = 1000
}
} else {
fb.mediaType = mime.TypeByExtension(filepath.Ext(fb.Name))
}
}
// FromJSON create FileBox from JSON
func FromJSON(s string) *FileBox {
options := new(Options)
if err := json.Unmarshal([]byte(s), options); err != nil {
err = fmt.Errorf("FromJSON json.Unmarshal: %w", err)
return newFileBox(TypeUnknown, &fileBoxUnknown{}, newOptions()).setErr(err)
}
// 对未来要弃用的 json.BoxTypeDeprecated 做兼容处理
if options.BoxTypeDeprecated != 0 && options.BoxType == 0 {
options.BoxType = options.BoxTypeDeprecated
}
switch options.BoxType {
case TypeBase64:
return FromBase64(options.Base64, WithOptions(*options))
case TypeQRCode:
return FromQRCode(options.QrCode, WithOptions(*options))
case TypeUrl:
return FromUrl(options.RemoteUrl, WithOptions(*options))
case TypeUuid:
return FromUuid(options.Uuid, WithOptions(*options))
default:
err := fmt.Errorf("FromJSON invalid value boxType: %v", options.BoxType)
return newFileBox(TypeUnknown, &fileBoxUnknown{}, newOptions()).setErr(err)
}
}
// FromBase64 create FileBox from Base64
func FromBase64(encode string, options ...Option) *FileBox {
var err error
if encode == "" {
err = fmt.Errorf("FromBase64 %w", ErrNoBase64Data)
}
o := newOptions(options...)
if o.Name == "" {
o.Name = "base64.dat"
}
o.Size = helper.Base64OrigLength(encode)
return newFileBox(TypeBase64,
newFileBoxBase64(encode), o).setErr(err)
}
// FromUrl create FileBox from url
func FromUrl(urlString string, options ...Option) *FileBox {
var err error
if urlString == "" {
err = fmt.Errorf("FromUrl %w", ErrNoUrl)
}
o := newOptions(options...)
if o.Name == "" && err == nil {
if u, e := url.Parse(urlString); e != nil {
err = e
} else {
o.Name = strings.TrimLeft(u.Path, "/")
}
}
return newFileBox(TypeUrl,
newFileBoxUrl(urlString, o.Headers), o).setErr(err)
}
// FromFile create FileBox from file
func FromFile(path string, options ...Option) *FileBox {
var err error
if path == "" {
err = fmt.Errorf("FromFile %w", ErrNoPath)
}
o := newOptions(options...)
if o.Name == "" {
o.Name = path2.Base(path)
}
if err == nil {
if file, e := os.Stat(path); e != nil {
err = e
} else {
o.Size = file.Size()
}
}
return newFileBox(TypeFile,
newFileBoxFile(path), o).setErr(err)
}
// FromQRCode create FileBox from QRCode
func FromQRCode(qrCode string, options ...Option) *FileBox {
var err error
if qrCode == "" {
err = fmt.Errorf("FromQRCode %w", ErrNoQRCode)
}
return newFileBox(TypeQRCode,
newFileBoxQRCode(qrCode),
newOptions(append(options, WithName("qrcode.png"))...)).setErr(err)
}
// FromStream from io.Reader
func FromStream(reader io.Reader, options ...Option) *FileBox {
o := newOptions(options...)
if o.Name == "" {
o.Name = "stream.dat"
}
return newFileBox(TypeStream,
newFileBoxStream(reader), o)
}
func FromUuid(uuid string, options ...Option) *FileBox {
var err error
if uuid == "" {
err = fmt.Errorf("FromUuid %w", ErrNoUuid)
}
o := newOptions(options...)
if o.Name == "" {
o.Name = uuid + ".dat"
}
return newFileBox(TypeUuid, newFileBoxUuid(uuid), o).setErr(err)
}
// ToJSON to json string
func (fb *FileBox) ToJSON() (string, error) {
if fb.err != nil {
return "", fb.err
}
jsonMap := map[string]interface{}{
"name": fb.Name,
"metadata": fb.metadata,
"type": fb.boxType,
"boxType": fb.boxType, //Deprecated
"size": fb.size,
"md5": fb.md5,
"mediaType": fb.mediaType,
}
switch fb.boxType {
case TypeUrl, TypeQRCode, TypeBase64, TypeUuid:
break
default:
return "", ErrToJSON
}
implJsonMap, err := fb.fileImpl.toJSONMap()
if err != nil {
return "", err
}
for k, v := range implJsonMap {
jsonMap[k] = v
}
marshal, err := json.Marshal(jsonMap)
return string(marshal), err
}
// ToFile save to file
func (fb *FileBox) ToFile(filePath string, overwrite bool) error {
if fb.err != nil {
return fb.err
}
if filePath == "" {
filePath = fb.Name
}
path, err := os.Getwd()
if err != nil {
return err
}
fullPath := filepath.Join(path, filePath)
if !overwrite && helper.FileExists(fullPath) {
return os.ErrExist
}
reader, err := fb.ToReader()
if err != nil {
return err
}
writer := bufio.NewReader(reader)
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
return err
}
if _, err := writer.WriteTo(file); err != nil {
return err
}
return nil
}
// ToBytes to bytes
func (fb *FileBox) ToBytes() ([]byte, error) {
if fb.err != nil {
return nil, fb.err
}
reader, err := fb.ToReader()
if err != nil {
return nil, err
}
return ioutil.ReadAll(reader)
}
// ToBase64 to base64 string
func (fb *FileBox) ToBase64() (string, error) {
if fb.err != nil {
return "", fb.err
}
if fb.boxType == TypeBase64 {
return fb.fileImpl.(*fileBoxBase64).base64Data, nil
}
fileBytes, err := fb.ToBytes()
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(fileBytes), nil
}
// ToDataURL to dataURL
func (fb *FileBox) ToDataURL() (string, error) {
if fb.err != nil {
return "", fb.err
}
toBase64, err := fb.ToBase64()
if err != nil {
return "", nil
}
return fmt.Sprintf("data:%s;base64,%s", fb.mediaType, toBase64), nil
}
// ToQRCode to QRCode
func (fb *FileBox) ToQRCode() (string, error) {
if fb.err != nil {
return "", fb.err
}
reader, err := fb.ToReader()
if err != nil {
return "", err
}
decode, err := qrcode.Decode(reader)
if err != nil {
return "", nil
}
return decode.Content, nil
}
// ToUuid to uuid
func (fb *FileBox) ToUuid() (string, error) {
if fb.err != nil {
return "", fb.err
}
if fb.boxType == TypeUuid {
return fb.fileImpl.(*fileBoxUuid).uuid, nil
}
reader, err := fb.ToReader()
if err != nil {
return "", err
}
if uuidFromStream == nil {
return "", errors.New("need to use filebox.SetUuidSaver() before dealing with UUID")
}
return uuidFromStream(reader)
}
// String ...
func (fb *FileBox) String() string {
return fmt.Sprintf("FileBox#%s<%s>", fb.boxType, fb.Name)
}
// ToReader to io.Reader
func (fb *FileBox) ToReader() (io.Reader, error) {
if fb.err != nil {
return nil, fb.err
}
return fb.fileImpl.toReader()
}
// Type get type
func (fb *FileBox) Type() Type {
return fb.boxType
}
// MetaData get metadata
func (fb *FileBox) MetaData() map[string]interface{} {
// TODO deep copy?
return fb.metadata
}
// Error ret err
func (fb *FileBox) Error() error {
return fb.err
}
func (fb *FileBox) setErr(err error) *FileBox {
fb.err = err
return fb
}
func (fb *FileBox) Size() {
}