提交 ed5697ec 编写于 作者: P peterq

add: md5方式分享文件接口

上级 c65dacdd
......@@ -3,6 +3,7 @@ package functions
import (
"fmt"
"github.com/peterq/pan-light/pc/pan-api"
"github.com/peterq/pan-light/pc/pan-download"
)
func init() {
......@@ -48,4 +49,12 @@ var panApiAsyncRoutes = map[string]asyncHandler{
resolve(result)
}
},
"pan.rapid.md5": func(p map[string]interface{}, resolve func(interface{}), reject func(interface{}), progress func(interface{}), qmlMsg chan interface{}) {
sliceMd5, err := pan_download.RapidUploadMd5(fmt.Sprint(int(p["fid"].(float64))))
if err != nil {
reject(err)
return
}
resolve(sliceMd5)
},
}
......@@ -445,6 +445,17 @@ var openFeedback = (function () {
}
})()
var openShare = (function () {
var comp = loadComponent(function(){},'../pages/share-window.qml')
var ins
return function(meta){
if (!ins || !ins.visible) {
ins = comp.createObject(G.root, {meta: meta})
}
return ins
}
})()
function api(name, param) {
param = param || {}
return callGoAsync('api.call', {name: name, param: param})
......
......@@ -5,7 +5,7 @@ Item {
property var tabsUrl: {'我的网盘': 'pan', '传输列表': 'transfer', '探索': 'explore'}
property var tabs: ['我的网盘', '传输列表', '探索']
property var colors: ['blue', 'red', 'green']
property string activeTab: '传输列表'
property string activeTab: '我的网盘'
anchors.fill: parent
Header {
id: header
......
import QtQuick 2.0
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import "../js/app.js" as App
import "../js/util.js" as Util
import "../widget"
Window {
id: window
flags: Qt.Dialog | Qt.WindowModal | Qt.WindowCloseButtonHint
modality: Qt.ApplicationModal
title: '分享到资源广场'
minimumHeight: height
minimumWidth: width
maximumHeight: height
maximumWidth: width
visible: true
width: 550
height: 300
property bool submitting: flase
property var sliceMd5Promise
property var meta
property var timeMap: {
"永久": 365,
"7天": 7,
"一个月": 30
}
property string fileExtention
Component.onCompleted: {
getSliceMd5()
}
function getSliceMd5() {
sliceMd5Promise = Util.callGoAsync('pan.rapid.md5', {
"fid": meta.fs_id
}).then(function (sliceMd5) {
console.log('slice md5',
sliceMd5)
return sliceMd5
})
sliceMd5Promise.catch(function () {
sliceMd5Promise = null
})
}
TopIndicator {
id: topIndicator
z: 2
}
GridLayout {
columns: 2
width: parent.width - 20
y: 20
rowSpacing: 10
anchors.horizontalCenter: parent.horizontalCenter
Label {
text: '分享文件名'
width: parent.width * 30
Layout.alignment: Qt.AlignRight
}
TextField {
id: titleInput
enabled: !window.submitting
width: parent.width * 60
placeholderText: "文件名"
Component.onCompleted: {
var t = meta.server_filename.split('.')
window.fileExtention = t.pop()
text = t.join('.')
}
}
Label {
text: '有效期'
width: parent.width * 30
Layout.alignment: Qt.AlignRight
}
ComboBox {
id: selectDuraion
enabled: !window.submitting
model: Object.keys(window.timeMap)
}
Label {
}
Button {
id: btnShare
text: window.submitting ? '请稍后' : '分享'
enabled: !submitting
onClicked: {
if (!titleInput.text)
return
if (!sliceMd5Promise) {
getSliceMd5()
}
window.submitting = true
sliceMd5Promise.then(function (sliceMd5) {
return Util.api('share', {
"md5": window.meta.md5,
"sliceMd5": sliceMd5,
"title": titleInput.text + '.' + window.fileExtention,
"duration": window.timeMap[selectDuraion.currentText],
"fileSize": window.meta.size
})
})
.then(function() {
topIndicator.success('分享成功')
return Util.sleep(1500)
})
.then(function(){
window.visible = false
})
.catch(function (err) {
topIndicator.fail(err.message)
window.submitting = false
})
}
}
}
}
......@@ -99,6 +99,11 @@ Item {
"cb": function () {
fileItem.clickDownloadViaVip()
}
}, {
"name": '分享到资源广场',
"cb": function () {
fileItem.clickShare()
}
}]
if (Util.isVideo(fileItem.meta.server_filename)) {
fileItem.menus = fileItem.menus.concat([{
......@@ -152,4 +157,7 @@ Item {
console.log('vip down')
App.appState.transferComp.addDownload(fileItem.meta, true)
}
function clickShare() {
Util.openShare(fileItem.meta)
}
}
......@@ -168,5 +168,6 @@
<file>pages/about-window.qml</file>
<file>pages/feedback-window.qml</file>
<file>widget/TopIndicator.qml</file>
<file>pages/share-window.qml</file>
</qresource>
</RCC>
......@@ -8,13 +8,15 @@ Rectangle {
property alias duration: hideTimer.interval
width: parent.width
height: 40
height: 10 + msgText.implicitHeight
y: -height
Text {
id: msgText
text: ''
color: 'white'
width: parent.width - 20
wrapMode: Text.WrapAnywhere
anchors.centerIn: parent
}
......
......@@ -122,7 +122,7 @@ func BaiduCookieLogin(cookieStr string) error {
if j := strings.Index(name, "="); j >= 0 {
name, val = name[:j], name[j+1:]
}
cookies = append(cookies, &http.Cookie{Name: name, Value: val})
cookies = append(cookies, &http.Cookie{Name: name, Value: val, Domain: ".baidu.com"})
}
u, _ := url.Parse("https://pan.baidu.com")
......
......@@ -6,6 +6,8 @@ import (
"github.com/peterq/pan-light/pc/dep"
"github.com/peterq/pan-light/pc/downloader"
"github.com/peterq/pan-light/pc/pan-api"
"github.com/peterq/pan-light/pc/util"
"github.com/pkg/errors"
"io/ioutil"
"log"
"net/http"
......@@ -76,6 +78,37 @@ func DownloadFile(fid, savePath string, useVip bool) (taskId downloader.TaskId,
return
}
func RapidUploadMd5(fid string) (sliceMd5 string, err error) {
link, err := pan_api.Link(fid)
if err != nil {
err = errors.Wrap(err, "解析直链错误")
return
}
req, err := http.NewRequest("GET", link, nil)
if err != nil {
errors.Wrap(err, "无法创建request")
return
}
requestDecorator(req)
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", 0, 256*1024-1))
resp, err := manager.HttpClient.Do(req)
if err != nil {
err = errors.Wrap(err, "访问直链错误")
return
}
defer resp.Body.Close()
bin, err := ioutil.ReadAll(resp.Body)
if err != nil {
err = errors.Wrap(err, "获取前256k内容错误")
return
}
if len(bin) != 256*1024 {
err = errors.New("文件内容小于256k")
}
sliceMd5 = util.Md5bin(bin)
return
}
func requestDecorator(request *http.Request) *http.Request {
request.Header.Set("User-Agent", pan_api.BaiduUA)
return request
......
......@@ -19,6 +19,7 @@ var urlMap = map[string]string{
"login": "/api/pc/login",
"feedback": "/api/pc/feedback",
"refresh-token": "/api/pc/refresh-token",
"share": "/api/pc/share",
}
var httpClient = http.Client{
//Timeout: 15 * time.Second,
......
package util
import (
"crypto/md5"
"encoding/hex"
"math/rand"
"strconv"
"time"
......@@ -18,3 +20,9 @@ func First(args ...interface{}) interface{} {
func Second(args ...interface{}) interface{} {
return args[1]
}
func Md5bin(bin []byte) string {
h := md5.New()
h.Write(bin)
return hex.EncodeToString(h.Sum(nil))
}
......@@ -181,6 +181,13 @@ func (v JsonValue) Int() int {
panic(NewError(fmt.Sprintf("%s needs to be int, %T given", v.name, v.data), -1, nil))
}
func (v JsonValue) Int64() int64 {
if m, ok := v.data.(float64); ok {
return int64(m)
}
panic(NewError(fmt.Sprintf("%s needs to be int, %T given", v.name, v.data), -1, nil))
}
func (v JsonValue) Float() float64 {
if m, ok := v.data.(float64); ok {
return m
......
......@@ -5,6 +5,7 @@ const (
CollectionVipSaveFile = "vip_save_file"
CollectionUser = "user"
CollectionFeedback = "feedback"
CollectionFileShare = "file_share"
)
const (
......
package dao
import (
"github.com/peterq/pan-light/server/conf"
"gopkg.in/mgo.v2"
)
type FileShareModel struct {
Uk string
Title string
Md5 string
SliceMd5 string `bson:"slice_md5"`
FileSize int64 `bson:"file_size"`
ExpireAt int64 `bson:"expire_at"`
}
type fileShareDao struct{}
func (*fileShareDao) collection(s *mgo.Session) *mgo.Collection {
return s.DB(conf.Conf.Database).C(conf.CollectionFileShare)
}
func (d *fileShareDao) Insert(data FileShareModel) (err error) {
s := conf.MongodbSession.Clone()
defer s.Refresh()
collection := d.collection(s)
err = collection.Insert(data)
return
}
var FileShareDao = &fileShareDao{}
package dao
import (
"github.com/peterq/pan-light/server/conf"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"time"
)
type VipSaveFileModel struct {
Id bson.ObjectId `bson:"_id,omitempty"`
Username string `bson:"username,omitempty"` // vip 用户名
Md5 string `bson:"md5,omitempty"`
SliceMd5 string `bson:"slice_md5,omitempty"`
FileSize int64 `bson:"file_size,omitempty"`
Fid string `bson:"fid,omitempty"`
AddAt int64 `bson:"add_at,omitempty"` // 转存时间
HitAt int64 `bson:"hit_at,omitempty"` // 重复转存命中时间
DeletedAt int64 `bson:"deleted_at"` // 删除时间
}
func (f *VipSaveFileModel) GetSavePath() string {
return "/pan-light-save/" + f.Md5[:2] + "/" + f.Md5[2:4]
}
type vipSaveFileDao struct{}
func (*vipSaveFileDao) collection(s *mgo.Session) *mgo.Collection {
return s.DB(conf.Conf.Database).C(conf.CollectionVipSaveFile)
}
func (d *vipSaveFileDao) GetByMd5(md5 string) (data VipSaveFileModel, err error) {
s := conf.MongodbSession.Clone()
defer s.Refresh()
collection := d.collection(s)
err = collection.Pipe([]bson.M{
{
"$match": bson.M{
"deleted_at": 0,
},
},
{
"$lookup": bson.M{
"from": conf.CollectionVip,
"localField": "username",
"foreignField": "username",
"as": "viper",
},
},
{
"$match": bson.M{
"viper.enabled": true,
},
},
}).One(&data)
return
}
func (d *vipSaveFileDao) Insert(data VipSaveFileModel) (err error) {
s := conf.MongodbSession.Clone()
defer s.Refresh()
collection := d.collection(s)
err = collection.Insert(data)
return
}
func (d *vipSaveFileDao) Hit(data VipSaveFileModel) (err error) {
data.HitAt = time.Now().Unix()
s := conf.MongodbSession.Clone()
defer s.Refresh()
collection := d.collection(s)
err = collection.Update(bson.M{
"_id": data.Id,
}, bson.M{
"$set": bson.M{
"hit_at": data.HitAt,
},
})
return
}
var VipSaveFileDao = &vipSaveFileDao{}
......@@ -3,11 +3,13 @@ package dao
import (
"github.com/peterq/pan-light/server/conf"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
type VipModel struct {
Username string
Bduss string
Cookie string
Enabled bool
}
type vipDao struct{}
......@@ -20,7 +22,9 @@ func (d *vipDao) GetAll() (data []VipModel, err error) {
s := conf.MongodbSession.Clone()
defer s.Refresh()
collection := d.collection(s)
err = collection.Find(nil).All(&data)
err = collection.Find(bson.M{
"enabled": true,
}).All(&data)
return
}
......
......@@ -16,17 +16,23 @@ type gson = map[string]interface{}
const baiduUa = "netdisk;4.6.2.0;PC;PC-Windows;10.0.10240;WindowsBaiduYunGuanJia"
func makeHttpClient(bduss string) http.Client {
func makeHttpClient(cookieStr string) http.Client {
jar, _ := cookiejar.New(nil)
u, _ := url.Parse("https://pan.baidu.com")
jar.SetCookies(u, []*http.Cookie{
{
Name: "BDUSS",
Value: bduss,
Path: "/",
Domain: ".baidu.com",
},
})
var cookies []*http.Cookie
parts := strings.Split(strings.TrimSpace(cookieStr), ";")
for i := 0; i < len(parts); i++ {
parts[i] = strings.TrimSpace(parts[i])
if len(parts[i]) == 0 {
continue
}
name, val := parts[i], ""
if j := strings.Index(name, "="); j >= 0 {
name, val = name[:j], name[j+1:]
}
cookies = append(cookies, &http.Cookie{Name: name, Value: val, Domain: ".baidu.com"})
}
jar.SetCookies(u, cookies)
httpClient := http.Client{
Transport: nil,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
......
package pan_viper
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/kataras/iris"
"github.com/peterq/pan-light/server/dao"
"github.com/pkg/errors"
"io/ioutil"
"log"
"net/http"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
)
var vipMap = map[string]*Vip{}
var vipMap sync.Map
func init() {
data, err := dao.Vip.GetAll()
if err != nil {
panic(err)
}
if len(data) == 0 {
panic("no vip account available")
}
for _, model := range data {
vipMap[model.Username] = &Vip{
http: makeHttpClient(model.Bduss),
username: model.Username,
v := &Vip{
username: model.Username,
cookieRaw: model.Cookie,
}
v.init()
vipMap.Store(model.Username, v)
}
}
type loginSession struct {
Sign string
Timestamp string
Bdstoken string
Bduss string
}
type Vip struct {
http http.Client
username string
_loginSession loginSession
bduss string
cookieRaw string
_loginSession *loginSession
loginSessionLock sync.RWMutex
}
func (v *Vip) loginSession() loginSession {
func (v *Vip) loginSession() *loginSession {
v.loginSessionLock.RLock()
defer v.loginSessionLock.RUnlock()
return v._loginSession
}
func (v *Vip) init() {
v.http = makeHttpClient(v.cookieRaw)
go v.CreateSession()
}
func (v *Vip) Username() string {
return v.username
}
func (v *Vip) CreateSession() {
//v.request()
func (v *Vip) CreateSession() (err error) {
defer func() {
if err != nil {
(&iris.Application{}).Logger().Error(v.username, "vip创建session错误", err)
}
}()
old := v.loginSession()
v.loginSessionLock.Lock()
defer v.loginSessionLock.Unlock()
// 高并发下防止重复更新session
if old != v._loginSession {
return
}
homePageLink := "https://pan.baidu.com/disk/homePageLink"
req := newRequest("GET", homePageLink)
res, err := v.http.Do(req)
if err != nil {
err = errors.Wrap(err, "访问首页错误")
return
}
bin, err := ioutil.ReadAll(res.Body)
if err != nil {
err = errors.Wrap(err, "读取首页body错误")
return
}
body := string(bin)
reg := regexp.MustCompile(`var context=(.*);\n`)
find := reg.FindStringSubmatch(body)
if res.Request.URL.String() != homePageLink {
err = errors.New("重定向到" + res.Request.URL.String())
return
}
raw := gson{}
err = json.Unmarshal([]byte(find[1]), &raw)
if err != nil {
log.Println(body)
return
}
s := loginSession{
Sign: "",
Timestamp: "",
Bdstoken: "",
Bduss: "",
}
s.Sign = loginSign(raw["sign3"].(string), raw["sign1"].(string))
s.Timestamp = fmt.Sprint(int(raw["timestamp"].(float64)))
s.Bdstoken = raw["bdstoken"].(string)
s.Bduss = v.bduss
v._loginSession = &s
log.Println(v.username, "完成loginSession")
return
}
func loginSign(j, r string) string {
a := [256]int{}
p := [256]int{}
o := make([]byte, len(r))
v := len(j)
for q := 0; q < 256; q++ {
a[q] = int(j[q%v : q%v+1][0])
p[q] = q
}
for u, q := 0, 0; q < 256; q++ {
u = (u + p[q] + a[q]) % 256
t := p[q]
p[q] = p[u]
p[u] = t
}
for i, u, q := 0, 0, 0; q < len(r); q++ {
i = (i + 1) % 256
u = (u + p[i]) % 256
t := p[i]
p[i] = p[u]
p[u] = t
k := p[((p[i] + p[u]) % 256)]
o[q] = byte(int(r[q : q+1][0]) ^ k)
}
return base64.StdEncoding.EncodeToString(o)
}
func (v *Vip) LoadShareFilenameAndUk(link, secret string) (uk, filename string, share gson, err error) {
......@@ -88,6 +184,31 @@ func (v *Vip) LoadShareFilenameAndUk(link, secret string) (uk, filename string,
return
}
func (v *Vip) SaveFileByMd5(md5, sliceMd5, path string, contentLength int64) (fid string, fileSize int64, err error) {
ss := v.loginSession()
data, err := v.request("POST", "https://pan.baidu.com/api/rapidupload", gson{
"rtype": 1,
"channel": "chunlei",
"web": 1,
"app_id": 250528,
"bdstoken": ss.Bdstoken,
"logid": time.Now().UnixNano(),
"clienttype": 0,
}, gson{
"path": path,
"content-length": contentLength,
"content-md5": md5,
"slice-md5": sliceMd5,
"target_path": filepath.Dir(path),
"local_mtime": 1533345687,
})
if _, ok := data["errno"]; !ok {
log.Println(data)
err = errors.New("极速上传失败")
}
return
}
func (v *Vip) inputSharePwd(link, secret string) (err error) {
t := strings.Split(link, "/")
......@@ -138,14 +259,27 @@ func (v *Vip) request(method, link string, params gson, form gson) (data gson, e
return
}
if n, ok := data["errno"]; ok && n.(float64) != 0 {
// 页面过期错误码
if n.(float64) == 112 {
go v.CreateSession()
}
err = errors.New("pan api error code " + fmt.Sprint(data["errno"]))
}
return
}
func GetVip() *Vip {
for _, value := range vipMap {
return value
var v *Vip
vipMap.Range(func(key, value interface{}) bool {
v = value.(*Vip)
return false
})
return v
}
func GetVipByUsername(username string) *Vip {
if v, ok := vipMap.Load(username); ok {
return v.(*Vip)
}
return nil
}
......@@ -9,6 +9,8 @@ import (
"github.com/peterq/pan-light/server/dao"
"github.com/peterq/pan-light/server/pan-viper"
"github.com/peterq/pan-light/server/pc-api/middleware"
"gopkg.in/mgo.v2"
"log"
"strings"
"time"
)
......@@ -94,7 +96,70 @@ func handleFeedBack(ctx context.Context, param artisan.JsonMap) (result interfac
Content: content,
})
if err != nil {
err = artisan.NewError("database error", -1, nil)
err = artisan.NewError("database error", -1, err)
}
return
}
func handleShareToSquare(ctx context.Context, param artisan.JsonMap) (result interface{}, err error) {
md5 := param.Get("md5").String()
sliceMd5 := param.Get("sliceMd5").String()
title := param.Get("title").String()
duration := param.Get("duration").Int()
fileSize := param.Get("fileSize").Int64()
// 查找该文件是否被vip账号存储过
data, err := dao.VipSaveFileDao.GetByMd5(md5)
if err != nil && err != mgo.ErrNotFound {
err = artisan.NewError("database error", -1, err)
return
}
log.Println(data, err)
// 没有存储过, 使用秒传进行存储
if err == mgo.ErrNotFound {
data = dao.VipSaveFileModel{
Username: "",
Md5: md5,
SliceMd5: sliceMd5,
FileSize: 0,
Fid: "",
AddAt: time.Now().Unix(),
HitAt: time.Now().Unix(),
DeletedAt: 0,
}
viper := pan_viper.GetVip()
data.Username = viper.Username()
data.Fid, data.FileSize, err = viper.SaveFileByMd5(md5, sliceMd5, data.GetSavePath(), fileSize)
if err != nil {
err = artisan.NewError("vip账号转存文件错误", -1, err)
return
}
err = dao.VipSaveFileDao.Insert(data)
if err != nil {
err = artisan.NewError("database error", -1, err)
return
}
} else { // 存储过, 更新命中时间戳
err = dao.VipSaveFileDao.Hit(data)
if err != nil {
err = artisan.NewError("database error", -1, err)
return
}
}
// 写入分享表
share := dao.FileShareModel{
Uk: middleware.ContextLoginInfo(ctx).Uk(),
Title: title,
Md5: md5,
SliceMd5: sliceMd5,
FileSize: data.FileSize,
ExpireAt: time.Now().Add(time.Hour * 24 * time.Duration(duration)).Unix(),
}
dao.FileShareDao.Insert(share)
if err != nil {
err = artisan.NewError("database error", -1, err)
return
}
result = share
return
}
......@@ -48,4 +48,9 @@ func pcAuthRoutes(r router.Party) {
}, handleFeedBack)
post("refresh-token", handleRefreshToken)
post("share", artisan.ThrottleOption{
Duration: time.Hour,
Number: 500,
}, handleShareToSquare)
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册