├── .gitignore ├── 115pan ├── .gitignore ├── 115pan.png ├── Makefile ├── model.go ├── plugin.go ├── plugin.toml ├── plugin_impl.go └── plugin_impl_test.go ├── Makefile ├── README.md ├── alipan ├── .gitignore ├── Makefile ├── alipan.png ├── model.go ├── plugin.go ├── plugin.toml ├── plugin_impl.go └── plugin_impl_test.go ├── alist ├── .gitignore ├── Makefile ├── alist.png ├── model.go ├── plugin.go ├── plugin.toml ├── plugin_impl.go └── plugin_impl_test.go ├── baidupan ├── .gitignore ├── Makefile ├── baidupan.png ├── model.go ├── plugin.go ├── plugin.toml ├── plugin_impl.go └── plugin_impl_test.go ├── build.sh ├── ftp ├── .gitignore ├── Makefile ├── ftp.png ├── plugin.go ├── plugin.toml └── plugin_impl.go ├── go.mod ├── go.sum ├── local ├── .gitignore ├── Makefile ├── local.png ├── plugin.go ├── plugin.toml └── plugin_impl.go ├── quark ├── .gitignore ├── Makefile ├── model.go ├── plugin.go ├── plugin.toml ├── plugin_impl.go ├── plugin_impl_test.go └── quark.png ├── sftp ├── .gitignore ├── Makefile ├── plugin.go ├── plugin.toml ├── plugin_impl.go └── sftp.png ├── smb ├── .gitignore ├── LICENSE ├── Makefile ├── plugin.go ├── plugin.toml ├── plugin_impl.go ├── plugin_impl_test.go └── smb.png ├── util ├── auth.go ├── auth_test.go └── env.json └── webdav ├── .gitignore ├── Makefile ├── plugin.go ├── plugin.toml ├── plugin_impl.go ├── plugin_impl_test.go └── webdav.png /.gitignore: -------------------------------------------------------------------------------- 1 | */*/*.zip 2 | util/env.json 3 | -------------------------------------------------------------------------------- /115pan/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /115pan/115pan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/115pan/115pan.png -------------------------------------------------------------------------------- /115pan/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /115pan/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // https://www.yuque.com/115yun/open/um8whr91bxb5997o 4 | 5 | const ( 6 | Api115PanAddr = "https://proapi.115.com/" 7 | ) 8 | 9 | type QrResponse struct { 10 | State int `json:"state"` 11 | Code int `json:"code"` 12 | Message string `json:"message"` 13 | Data any `json:"data"` 14 | Error string `json:"error"` 15 | Errno int `json:"errno"` 16 | } 17 | 18 | type Response struct { 19 | State any `json:"state"` 20 | Code int `json:"code"` 21 | Message string `json:"message"` 22 | Data any `json:"data"` 23 | Error string `json:"error"` 24 | Errno int `json:"errno"` 25 | } 26 | 27 | type AuthDeviceCodeData struct { 28 | Uid string `json:"uid"` 29 | Time int64 `json:"time"` 30 | Qrcode string `json:"qrcode"` 31 | Sign string `json:"sign"` 32 | } 33 | type UserInfo struct { 34 | UserId int `json:"user_id"` 35 | UserName string `json:"user_name"` 36 | } 37 | 38 | type FileEntry struct { 39 | Fid string `json:"fid"` // if fid is empty,show / 40 | Aid string `json:"aid"` // 文件的状态,aid 的别名。1 正常,7 删除(回收站),120 彻底删除 41 | Pid string `json:"pid"` // 父目录ID 42 | Fc string `json:"fc"` // 0 folder 1 file 43 | Fn string `json:"fn"` // file name 44 | Pc string `json:"pc"` // file pick code 45 | // Isp string `json:"isp"` // passwd 46 | 47 | Upt uint64 `json:"upt"` // 修改时间 48 | Uppt uint64 `json:"uppt"` // 上传时间 49 | Fs uint64 `json:"fs"` // file size 50 | 51 | Fta string `json:"fta"` // 文件状态 0/2 未上传完成,1 已上传完成 52 | } 53 | 54 | type FileURL struct { 55 | FileName string `json:"file_name"` 56 | FileSize uint64 `json:"file_size"` 57 | PickCode string `json:"pick_code"` 58 | Sha1 string `json:"sha1"` 59 | Url struct { 60 | Url string `json:"url"` 61 | } `json:"url"` 62 | } 63 | 64 | type SubtitleData struct { 65 | List []Subtitle `json:"list"` 66 | } 67 | 68 | type Subtitle struct { 69 | Sid string `json:"sid"` 70 | Language string `json:"language"` 71 | Title string `json:"title"` 72 | URL string `json:"url"` 73 | Type string `json:"type"` 74 | } 75 | 76 | type PlayVideoInfo struct { 77 | FileId string `json:"file_id"` 78 | FileName string `json:"file_name"` 79 | VideoURL []struct { 80 | URL string `json:"url"` 81 | Definition uint64 `json:"definition"` 82 | DefinitionN uint64 `json:"definition_n"` 83 | Title string `json:"title"` 84 | } `json:"video_url"` 85 | 86 | // TODO add audio 87 | // MultitrackList []struct { 88 | // Title string `json:"title"` 89 | // } 90 | } 91 | -------------------------------------------------------------------------------- /115pan/plugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by plugin_api. DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /115pan/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "115pan" 2 | name = "115Pan" 3 | desc = "115pan plugin" 4 | icon = "115pan.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.4" 7 | changelog = ["change file entry size type to uint64"] 8 | -------------------------------------------------------------------------------- /115pan/plugin_impl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "image/png" 10 | "io" 11 | "log/slog" 12 | "net/http" 13 | "net/url" 14 | "plugins/util" 15 | "strconv" 16 | "strings" 17 | "time" 18 | 19 | "github.com/boombuler/barcode" 20 | "github.com/boombuler/barcode/qr" 21 | "github.com/medianexapp/plugin_api/httpclient" 22 | "github.com/medianexapp/plugin_api/plugin" 23 | "github.com/medianexapp/plugin_api/ratelimit" 24 | 25 | _ "github.com/labulakalia/wazero_net/wasi/http" 26 | ) 27 | 28 | /* 29 | NOTE: net and http use package 30 | "github.com/labulakalia/wazero_net/wasi/http" 31 | "github.com/labulakalia/wazero_net/wasi/net" 32 | */ 33 | 34 | type PluginImpl struct { 35 | token *plugin.Token 36 | userInfo *UserInfo 37 | 38 | client *httpclient.Client 39 | ratelimit *ratelimit.RateLimit 40 | } 41 | 42 | func NewPluginImpl() *PluginImpl { 43 | slog.SetLogLoggerLevel(slog.LevelDebug) 44 | client := httpclient.NewClient() 45 | limitConfigMap := map[string]ratelimit.LimitConfig{ 46 | "/open/ufile/downurl": ratelimit.LimitConfig{ 47 | Limit: 1, 48 | Duration: 3 * time.Second, 49 | }, 50 | "/open/video/subtitle": ratelimit.LimitConfig{ 51 | Limit: 1, 52 | Duration: 3 * time.Second, 53 | }, 54 | "/open/video/play": ratelimit.LimitConfig{ 55 | Limit: 1, 56 | Duration: 3 * time.Second, 57 | }, 58 | "/open/ufile/files": ratelimit.LimitConfig{ 59 | Limit: 1, 60 | Duration: 1 * time.Second, 61 | }, 62 | } 63 | return &PluginImpl{ 64 | client: client, 65 | ratelimit: ratelimit.New(limitConfigMap), 66 | } 67 | } 68 | 69 | // Id implements IPlugin. 70 | func (p *PluginImpl) PluginId() (string, error) { 71 | return "115pan", nil 72 | } 73 | 74 | // GetAuthe implements IPlugin. 75 | // Note: not store var in GetAuth 76 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 77 | qrcodeData, err := util.GetAuthQrcode("115pan") 78 | if err != nil { 79 | return nil, err 80 | } 81 | authDeviceCodeData := &AuthDeviceCodeData{} 82 | resp := &QrResponse{ 83 | Data: authDeviceCodeData, 84 | } 85 | err = json.Unmarshal(qrcodeData, resp) 86 | if err != nil { 87 | return nil, err 88 | } 89 | auth := &plugin.Auth{ 90 | AuthMethods: []*plugin.AuthMethod{}, 91 | } 92 | if authDeviceCodeData.Qrcode != "" { 93 | qrCode, err := qr.Encode(authDeviceCodeData.Qrcode, qr.M, qr.Auto) 94 | if err != nil { 95 | return nil, err 96 | } 97 | qrCode, err = barcode.Scale(qrCode, 200, 200) 98 | if err != nil { 99 | return nil, err 100 | } 101 | buf := &bytes.Buffer{} 102 | err = png.Encode(buf, qrCode) 103 | if err != nil { 104 | return nil, err 105 | } 106 | p := map[string]string{ 107 | "uid": authDeviceCodeData.Uid, 108 | "time": fmt.Sprint(authDeviceCodeData.Time), 109 | "sign": authDeviceCodeData.Sign, 110 | } 111 | data, err := json.Marshal(p) 112 | if err != nil { 113 | return nil, err 114 | } 115 | scanQrcode := &plugin.AuthMethod_Scanqrcode{ 116 | Scanqrcode: &plugin.Scanqrcode{ 117 | QrcodeImage: buf.Bytes(), 118 | QrcodeImageParam: string(data), 119 | }, 120 | } 121 | auth.AuthMethods = append(auth.AuthMethods, &plugin.AuthMethod{ 122 | Method: scanQrcode, 123 | }) 124 | } 125 | url := util.GetAuthAddr("115pan") 126 | authCallbackUrl := &plugin.AuthMethod_Callback{ 127 | Callback: &plugin.Callback{ 128 | CallbackUrl: url, 129 | }, 130 | } 131 | auth.AuthMethods = append(auth.AuthMethods, &plugin.AuthMethod{ 132 | Method: authCallbackUrl, 133 | }) 134 | return auth, nil 135 | } 136 | 137 | // CheckAuth implements IPlugin. 138 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (*plugin.AuthData, error) { 139 | var ( 140 | token *plugin.Token 141 | err error 142 | ) 143 | switch v := authMethod.Method.(type) { 144 | case *plugin.AuthMethod_Refresh: 145 | token = &plugin.Token{} 146 | err = token.UnmarshalVT(v.Refresh.AuthData.AuthDataBytes) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | token, err = util.GetAuthToken(&util.GetAuthTokenRequest{ 152 | Id: "115pan", 153 | RefreshToken: token.RefreshToken, 154 | }) 155 | case *plugin.AuthMethod_Scanqrcode: 156 | token, err = util.CheckAuthQrcode("115pan", v.Scanqrcode.QrcodeImageParam) 157 | if err != nil { 158 | // if err,need refresh 159 | // 115pan qrcode if scan,qrcode will expire 160 | return nil, err 161 | } 162 | if token == nil { 163 | // scan not success 164 | slog.Warn("qrcode not scan success") 165 | return nil, nil 166 | } 167 | case *plugin.AuthMethod_Callback: 168 | slog.Info("recv callback data", "callBackData", v.Callback.CallbackUrlData) 169 | token = &plugin.Token{} 170 | urlParse, err := url.Parse(v.Callback.CallbackUrlData) 171 | if err != nil { 172 | return nil, err 173 | } 174 | data, err := base64.URLEncoding.DecodeString(urlParse.Query().Get("token")) 175 | if err != nil { 176 | return nil, err 177 | } 178 | err = token.UnmarshalVT(data) 179 | if err != nil { 180 | return nil, err 181 | } 182 | default: 183 | return nil, errors.New("unsupported auth method") 184 | } 185 | 186 | if err != nil { 187 | slog.Error("authCode to access token failed", "err", err) 188 | return nil, err 189 | } 190 | tokenBytes, err := token.MarshalVT() 191 | if err != nil { 192 | slog.Error("marshal token failed", "err", err) 193 | return nil, err 194 | } 195 | expireTime := time.Now().Add(time.Second * time.Duration(token.ExpiresIn-300)).Unix() 196 | authData := &plugin.AuthData{ 197 | AuthDataBytes: tokenBytes, 198 | AuthDataExpiredTime: uint64(expireTime), 199 | } 200 | slog.Info("get access token success") 201 | return authData, nil 202 | } 203 | 204 | // InitAuth implements IPlugin. 205 | func (p *PluginImpl) CheckAuthData(authDataBytes []byte) error { 206 | token := &plugin.Token{} 207 | err := token.UnmarshalVT(authDataBytes) 208 | if err != nil { 209 | slog.Error("unmarshal token failed", "err", err) 210 | return err 211 | } 212 | p.token = token 213 | 214 | resp := &Response{} 215 | err = p.send(http.MethodGet, "/open/user/info", nil, resp) 216 | if err != nil { 217 | slog.Error("get user info failed", "err", err) 218 | return err 219 | } 220 | if resp.State == false { 221 | slog.Error("get user info failed", "err", err) 222 | return errors.New(resp.Message) 223 | } 224 | data, err := json.Marshal(resp.Data) 225 | if err != nil { 226 | slog.Error("marshal user info failed", "err", err) 227 | return err 228 | } 229 | userInfo := &UserInfo{} 230 | err = json.Unmarshal(data, &userInfo) 231 | if err != nil { 232 | slog.Error("unmarshal user info failed", "err", err) 233 | return err 234 | } 235 | p.userInfo = userInfo 236 | return nil 237 | } 238 | 239 | // AuthId implements IPlugin. 240 | func (p *PluginImpl) PluginAuthId() (string, error) { 241 | if p.userInfo == nil { 242 | return "", errors.New("userInfo is nil") 243 | } 244 | return fmt.Sprint(p.userInfo.UserId), nil 245 | } 246 | 247 | // GetDirEntry implements IPlugin. 248 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 249 | slog.Debug("get dir entry ", "req", req) 250 | 251 | if req.Page == 0 { 252 | req.Page = 1 253 | } 254 | 255 | fileEntries := []*FileEntry{} 256 | resp := &Response{ 257 | Data: &fileEntries, 258 | } 259 | fid := "" 260 | if req.FileEntry != nil && req.FileEntry.RawData != nil { 261 | fileEntry := FileEntry{} 262 | err := json.Unmarshal(req.FileEntry.RawData, &fileEntry) 263 | if err != nil { 264 | return nil, err 265 | } 266 | fid = fileEntry.Fid 267 | } 268 | slog.Debug("get dir entry ", "fid", fid) 269 | 270 | u := url.Values{} 271 | u.Add("cid", fid) 272 | u.Add("show_dir", "1") 273 | u.Add("o", "file_name") 274 | u.Add("offset", fmt.Sprint((req.Page-1)*req.PageSize)) 275 | u.Add("limit", fmt.Sprint(req.PageSize)) 276 | u.Add("order", "file_name") 277 | p.ratelimit.Wait("/open/ufile/files") 278 | err := p.send(http.MethodGet, "/open/ufile/files?"+u.Encode(), nil, resp) 279 | if err != nil { 280 | return nil, err 281 | } 282 | slog.Debug("get send ") 283 | 284 | if resp.State == false { 285 | slog.Error("get dir failed", "err", err) 286 | return nil, errors.New(resp.Message) 287 | } 288 | dirEntry := &plugin.DirEntry{ 289 | FileEntries: []*plugin.FileEntry{}, 290 | } 291 | for _, fileEntry := range fileEntries { 292 | entry := &plugin.FileEntry{ 293 | Name: fileEntry.Fn, 294 | Size: uint64(fileEntry.Fs), 295 | CreatedTime: uint64(fileEntry.Uppt), 296 | ModifiedTime: uint64(fileEntry.Upt), 297 | AccessedTime: uint64(fileEntry.Upt), 298 | } 299 | entryBytes, err := json.Marshal(fileEntry) 300 | if err == nil { 301 | entry.RawData = entryBytes 302 | } 303 | if fileEntry.Fc == "0" { 304 | entry.FileType = plugin.FileEntry_FileTypeDir 305 | } else { 306 | entry.FileType = plugin.FileEntry_FileTypeFile 307 | } 308 | dirEntry.FileEntries = append(dirEntry.FileEntries, entry) 309 | } 310 | slog.Debug("return") 311 | 312 | return dirEntry, nil 313 | } 314 | 315 | // GetFileResource implements IPlugin. 316 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 317 | if req.FileEntry == nil || req.FileEntry.RawData == nil { 318 | return nil, fmt.Errorf("invalid path %s", req.FilePath) 319 | } 320 | 321 | fileResource := &plugin.FileResource{ 322 | FileResourceData: []*plugin.FileResource_FileResourceData{}} 323 | fileEntry := &FileEntry{} 324 | err := json.Unmarshal(req.FileEntry.RawData, fileEntry) 325 | if err != nil { 326 | return nil, err 327 | } 328 | reqURL := url.Values{} 329 | reqURL.Add("pick_code", fileEntry.Pc) 330 | 331 | respData := map[string]FileURL{} 332 | resp := Response{ 333 | Data: &respData, 334 | } 335 | p.ratelimit.Wait("/open/ufile/downurl") 336 | err = p.send(http.MethodPost, "/open/ufile/downurl", reqURL, &resp) 337 | if err != nil { 338 | return nil, err 339 | } 340 | if resp.State == true { 341 | fileURL, ok := respData[fileEntry.Fid] 342 | if ok { 343 | uu, err := url.Parse(fileURL.Url.Url) 344 | if err != nil { 345 | return nil, err 346 | } 347 | expireTime, err := strconv.ParseUint(uu.Query().Get("t"), 10, 0) 348 | if err != nil { 349 | return nil, err 350 | } 351 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 352 | Url: fileURL.Url.Url, 353 | Resolution: plugin.FileResource_Original, 354 | ResourceType: plugin.FileResource_Video, 355 | ExpireTime: expireTime, 356 | Header: map[string]string{ 357 | "User-Agent": httpclient.GetDefaultUserAgent(), 358 | }, 359 | }) 360 | } 361 | } else { 362 | slog.Error("get down file failed", "msg", resp.Message) 363 | } 364 | 365 | subtitleData := &SubtitleData{ 366 | List: []Subtitle{}, 367 | } 368 | resp = Response{ 369 | Data: &subtitleData, 370 | } 371 | p.ratelimit.Wait("/open/video/subtitle") 372 | err = p.send(http.MethodGet, "/open/video/subtitle?"+reqURL.Encode(), nil, &resp) 373 | if err != nil { 374 | return nil, err 375 | } 376 | for _, subtitle := range subtitleData.List { 377 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 378 | Url: subtitle.URL, 379 | ResourceType: plugin.FileResource_Subtitle, 380 | Title: subtitle.Title, 381 | Header: map[string]string{ 382 | "User-Agent": httpclient.GetDefaultUserAgent(), 383 | }, 384 | }) 385 | } 386 | 387 | playVideoInfo := PlayVideoInfo{} 388 | resp = Response{ 389 | Data: &playVideoInfo, 390 | } 391 | // get video play address 392 | p.ratelimit.Wait("/open/video/play") 393 | err = p.send(http.MethodGet, "/open/video/play?"+reqURL.Encode(), nil, &resp) 394 | if err != nil { 395 | return nil, err 396 | } 397 | if resp.State == true { 398 | for _, playVideoInfo := range playVideoInfo.VideoURL { 399 | data := &plugin.FileResource_FileResourceData{ 400 | Url: playVideoInfo.URL, 401 | ResourceType: plugin.FileResource_Video, 402 | Title: playVideoInfo.Title, 403 | Header: map[string]string{ 404 | "User-Agent": httpclient.GetDefaultUserAgent(), 405 | }, 406 | } 407 | if playVideoInfo.DefinitionN == 1 { 408 | data.Resolution = plugin.FileResource_SD 409 | } else if playVideoInfo.DefinitionN == 2 { 410 | data.Resolution = plugin.FileResource_LD 411 | } else if playVideoInfo.DefinitionN == 3 { 412 | data.Resolution = plugin.FileResource_HD 413 | } else if playVideoInfo.DefinitionN == 4 { 414 | data.Resolution = plugin.FileResource_FHD 415 | } else if playVideoInfo.DefinitionN == 4 { 416 | data.Resolution = plugin.FileResource_UHD 417 | } else if playVideoInfo.DefinitionN == 5 { 418 | data.Resolution = plugin.FileResource_Original 419 | } 420 | fileResource.FileResourceData = append(fileResource.FileResourceData, data) 421 | } 422 | } else { 423 | slog.Error("get play video info failed", "msg", resp.Message) 424 | } 425 | 426 | return fileResource, nil 427 | } 428 | 429 | func (p *PluginImpl) send(method string, uri string, req, resp any) error { 430 | var body io.Reader 431 | if req != nil { 432 | urlValue, ok := req.(url.Values) 433 | if !ok { 434 | return errors.New("req not is urlValues") 435 | } 436 | body = strings.NewReader(urlValue.Encode()) 437 | } 438 | 439 | httpReq, err := http.NewRequest(method, fmt.Sprintf("%s%s", Api115PanAddr, uri), body) 440 | if err != nil { 441 | return err 442 | } 443 | httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") 444 | if p.token == nil { 445 | return errors.New("token is nil") 446 | } 447 | httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.token.AccessToken)) 448 | httpResp, err := p.client.Do(httpReq) 449 | if err != nil { 450 | return err 451 | } 452 | 453 | bodyData, err := io.ReadAll(httpResp.Body) 454 | if err != nil { 455 | return err 456 | } 457 | slog.Debug("get request resp", "uri", uri, "req", req, "status_code", httpResp.StatusCode, "header", httpResp.Header, "resp", string(bodyData)) 458 | defer httpResp.Body.Close() 459 | 460 | err = json.Unmarshal(bodyData, resp) 461 | if err != nil { 462 | slog.Error("unmarshal body data failed", "err", err) 463 | return err 464 | } 465 | return nil 466 | } 467 | -------------------------------------------------------------------------------- /115pan/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "image/png" 6 | "os" 7 | "testing" 8 | 9 | "github.com/boombuler/barcode" 10 | "github.com/boombuler/barcode/qr" 11 | "github.com/medianexapp/plugin_api/plugin" 12 | ) 13 | 14 | func TestGenerateQrcode(t *testing.T) { 15 | // Create the barcode 16 | qrCode, _ := qr.Encode("Hello World", qr.M, qr.Auto) 17 | 18 | // Scale the barcode to 200x200 pixels 19 | qrCode, _ = barcode.Scale(qrCode, 200, 200) 20 | 21 | defer os.Remove("qrcode.png") 22 | 23 | // create the output file 24 | file, _ := os.Create("qrcode.png") 25 | defer file.Close() 26 | // encode the barcode as png 27 | png.Encode(file, qrCode) 28 | } 29 | 30 | func TestAuth(t *testing.T) { 31 | pluginImpl := NewPluginImpl() 32 | authData := `{}` 33 | token := &plugin.Token{} 34 | json.Unmarshal([]byte(authData), token) 35 | // t.Log(token) 36 | tokenData, _ := token.MarshalVT() 37 | err := pluginImpl.CheckAuthData(tokenData) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | dirEntry, err := pluginImpl.GetDirEntry(&plugin.GetDirEntryRequest{ 42 | Path: "/", 43 | Page: 1, 44 | PageSize: 10, 45 | }) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | for _, fileEntry := range dirEntry.FileEntries { 50 | if fileEntry.Name == "test.mkv" { 51 | fileResource, err := pluginImpl.GetFileResource(&plugin.GetFileResourceRequest{ 52 | FilePath: "/test.mkv", 53 | FileEntry: fileEntry, 54 | }) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | t.Logf("fileRe %+v \n", fileResource) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | bash build.sh 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Plugin For MediaNex 2 | -------------------------------------------------------------------------------- /alipan/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /alipan/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /alipan/alipan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/alipan/alipan.png -------------------------------------------------------------------------------- /alipan/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var ( 9 | AlipanURL = "https://openapi.alipan.com" 10 | ) 11 | 12 | type QrcodeResponse struct { 13 | QrCodeUrl string `json:"qrCodeUrl"` 14 | Sid string `json:"sid"` 15 | } 16 | 17 | type ErrResponse struct { 18 | Code string `json:"code"` 19 | Message string `json:"message"` 20 | RequestId string `json:"requestId"` 21 | } 22 | 23 | func (e ErrResponse) Error() string { 24 | return fmt.Sprintf("%s(%s)", e.Message, e.Code) 25 | } 26 | 27 | // /oauth/users/info 28 | type UserInfoResponse struct { 29 | Id string `json:"id"` 30 | Name string `json:"name"` 31 | Avator string `json:"avator"` 32 | Phone string `json:"phone"` 33 | } 34 | 35 | // /adrive/v1.0/user/getDriveInfo 36 | type UserGetDriverInfoResponse struct { 37 | Name string `json:"name"` 38 | DefaultDriverId string `json:"default_drive_id"` 39 | ResourceDriverId string `json:"resource_drive_id"` 40 | BackupDriverId string `json:"backup_drive_id"` 41 | } 42 | 43 | type FileEntry struct { 44 | DriveId string `json:"drive_id"` 45 | FileId string `json:"file_id"` 46 | ParentFileId string `json:"parent_file_id"` 47 | Name string `json:"name"` 48 | Size uint64 `json:"size"` 49 | ContentHash string `json:"content_hash"` 50 | Category string `json:"category"` 51 | Type string `json:"type"` 52 | CreatedTime time.Time `json:"created_at"` 53 | UpdatedTime time.Time `json:"updated_at"` 54 | } 55 | 56 | // 获取文件列表 57 | // /adrive/v1.0/openFile/list 58 | type OpenFileListRequest struct { 59 | DriveId string `json:"drive_id"` 60 | Limit int `json:"limit"` 61 | Marker string `json:"marker"` 62 | OrderBy string `json:"order_by"` // name_enhanced 63 | ParentFileId string `json:"parent_file_id"` 64 | Category string `json:"category"` 65 | } 66 | 67 | type OpenFileListResponse struct { 68 | NextMarker string `json:"next_marker"` 69 | Items []*FileEntry `json:"items"` 70 | } 71 | 72 | // /adrive/v1.0/openFile/get_by_path 73 | type OpenFilegetbypathRequest struct { 74 | DriveId string `json:"drive_id"` 75 | FilePath string `json:"file_path"` 76 | } 77 | 78 | type OpenFilegetbypathResponse struct { 79 | *FileEntry 80 | } 81 | 82 | // /adrive/v1.0/openFile/getDownloadUrl 83 | type OpenFilegetDownloadUrlRequest struct { 84 | DriveId string `json:"drive_id"` 85 | FileId string `json:"file_id"` 86 | ExpireSec int `json:"expire_sec"` 87 | } 88 | 89 | type OpenFilegetDownloadUrlResponse struct { 90 | Url string `json:"url"` 91 | Expiration string `json:"expiration"` 92 | Method string `json:"method"` 93 | } 94 | 95 | type CacheFileEntry struct { 96 | *FileEntry 97 | ExpireTime uint64 98 | } 99 | 100 | type OpenFileGetVideoPreviewPlayInfoRequest struct { 101 | DriveId string `json:"drive_id"` 102 | FileId string `json:"file_id"` 103 | Category string `json:"category"` // category 104 | TemplateId string `json:"template_id"` // HD|FHD|QHD 105 | UrlExpireSec int `json:"url_expire_sec"` // s 106 | GetSubtitleInfo bool `json:"get_subtitle_info"` 107 | } 108 | 109 | type OpenFileGetVideoPreviewPlayInfoResponse struct { 110 | DomainId string `json:"domain_id"` 111 | DriveId string `json:"drive_id"` 112 | FileId string `json:"file_id"` 113 | VideoPreViewPlayInfo *VideoPreViewPlayInfo `json:"video_preview_play_info"` 114 | } 115 | 116 | type VideoPreViewPlayInfo struct { 117 | Category string `json:"category"` // category 118 | LiveTranscodingTaskList []*LiveTranscodingTask `json:"live_transcoding_task_list"` 119 | LiveTranscodingSubtitleTaskList []*LiveTranscodingSubtitleTask `json:"live_transcoding_subtitle_task_list"` 120 | } 121 | 122 | type LiveTranscodingTask struct { 123 | TemplateId string `json:"template_id"` // HD|FHD|QHD 124 | Status string `json:"status"` // finished running failed 125 | Url string `json:"url"` 126 | } 127 | 128 | type LiveTranscodingSubtitleTask struct { 129 | Language string `json:"language"` 130 | Status string `json:"status"` // finished running failed 131 | Url string `json:"url"` 132 | } 133 | -------------------------------------------------------------------------------- /alipan/plugin.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /alipan/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "alipan" 2 | name = "Alipan" 3 | desc = "Alipan driver plugin" 4 | icon = "alipan.png" 5 | author = ["author1(labulakalia@gmail.com)"] 6 | version = "v0.0.2" 7 | changelog = ["add rate limit"] 8 | -------------------------------------------------------------------------------- /alipan/plugin_impl.go: -------------------------------------------------------------------------------- 1 | //go:build wasip1 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log/slog" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "plugins/util" 18 | "strings" 19 | "sync" 20 | "time" 21 | 22 | _ "github.com/labulakalia/wazero_net/wasi/http" 23 | "github.com/medianexapp/plugin_api/plugin" 24 | "github.com/medianexapp/plugin_api/ratelimit" 25 | ) 26 | 27 | /* 28 | NOTE: net and http use package 29 | "github.com/labulakalia/wazero_net/wasi/http" 30 | "github.com/labulakalia/wazero_net/wasi/net" 31 | */ 32 | 33 | type PluginImpl struct { 34 | oauthServerURL string 35 | 36 | token *plugin.Token 37 | userInfo *UserInfoResponse 38 | getDriverInfoResponse *UserGetDriverInfoResponse 39 | 40 | ratelimit *ratelimit.RateLimit 41 | } 42 | 43 | func NewPluginImpl() *PluginImpl { 44 | slog.SetLogLoggerLevel(slog.LevelDebug) 45 | limitConfigMap := map[string]ratelimit.LimitConfig{ 46 | "/adrive/v1.0/openFile/list": ratelimit.LimitConfig{ 47 | Limit: 40, 48 | Duration: 10 * time.Second, 49 | }, 50 | "/adrive/v1.0/openFile/getDownloadUrl": ratelimit.LimitConfig{ 51 | Limit: 1, 52 | Duration: time.Second, 53 | }, 54 | } 55 | return &PluginImpl{ 56 | ratelimit: ratelimit.New(limitConfigMap), 57 | } 58 | } 59 | 60 | // Id implements IPlugin. 61 | func (p *PluginImpl) PluginId() (string, error) { 62 | return "alipan", nil 63 | } 64 | 65 | func (p *PluginImpl) send(method string, uri string, req, resp any) error { 66 | _ = p.ratelimit.Wait(uri) 67 | var body io.Reader 68 | if req != nil { 69 | data, err := json.Marshal(req) 70 | if err != nil { 71 | return err 72 | } 73 | body = bytes.NewReader(data) 74 | } 75 | 76 | httpReq, err := http.NewRequest(method, fmt.Sprintf("%s%s", AlipanURL, uri), body) 77 | if err != nil { 78 | return err 79 | } 80 | httpReq.Header.Add("Content-Type", "application/json") 81 | if p.token != nil { 82 | httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.token.AccessToken)) 83 | } 84 | httpResp, err := http.DefaultClient.Do(httpReq) 85 | if err != nil { 86 | return err 87 | } 88 | bodyData, err := io.ReadAll(httpResp.Body) 89 | if err != nil { 90 | return err 91 | } 92 | slog.Debug("get request resp", "uri", uri, "req", req, "status_code", httpResp.StatusCode, "resp", string(bodyData)) 93 | defer httpResp.Body.Close() 94 | if httpResp.StatusCode != http.StatusOK { 95 | errResp := &ErrResponse{} 96 | err = json.Unmarshal(bodyData, errResp) 97 | if err != nil { 98 | return err 99 | } 100 | return errResp 101 | } 102 | err = json.Unmarshal(bodyData, resp) 103 | if err != nil { 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | func (p *PluginImpl) getQrcode() (*plugin.AuthMethod_Scanqrcode, error) { 110 | qrResp := &QrcodeResponse{} 111 | qrBytes, err := util.GetAuthQrcode("alipan") 112 | if err != nil { 113 | return nil, err 114 | } 115 | err = json.Unmarshal(qrBytes, qrResp) 116 | if err != nil { 117 | return nil, err 118 | } 119 | qrCodeResp, err := http.Get(qrResp.QrCodeUrl) 120 | if err != nil { 121 | return nil, err 122 | } 123 | defer qrCodeResp.Body.Close() 124 | if qrCodeResp.StatusCode != http.StatusOK { 125 | respData, err := io.ReadAll(qrCodeResp.Body) 126 | if err != nil { 127 | return nil, err 128 | } 129 | return nil, fmt.Errorf("get qrcode data failed: %s", respData) 130 | } 131 | qrcodeData, err := io.ReadAll(qrCodeResp.Body) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &plugin.AuthMethod_Scanqrcode{ 137 | Scanqrcode: &plugin.Scanqrcode{ 138 | QrcodeImage: qrcodeData, 139 | QrcodeImageParam: qrResp.Sid, 140 | QrcodeExpireTime: uint64(time.Now().Add(time.Minute * 3).Unix()), 141 | }, 142 | }, nil 143 | } 144 | 145 | // GetAuthe implements IPlugin. 146 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 147 | auth := &plugin.Auth{ 148 | AuthMethods: []*plugin.AuthMethod{}, 149 | } 150 | authScanQrcode, err := p.getQrcode() 151 | if err != nil { 152 | slog.Error("get qrcode failed", "err", err) 153 | return nil, err 154 | } 155 | 156 | auth.AuthMethods = append(auth.AuthMethods, &plugin.AuthMethod{ 157 | Method: authScanQrcode, 158 | }) 159 | 160 | url := util.GetAuthAddr("alipan") 161 | authCallbackUrl := &plugin.AuthMethod_Callback{ 162 | Callback: &plugin.Callback{ 163 | CallbackUrl: url, 164 | }, 165 | } 166 | auth.AuthMethods = append(auth.AuthMethods, &plugin.AuthMethod{ 167 | Method: authCallbackUrl, 168 | }) 169 | return auth, nil 170 | } 171 | 172 | // CheckAuth implements IPlugin. 173 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 174 | var ( 175 | token *plugin.Token 176 | authCode string 177 | refreshToken string 178 | state string 179 | ) 180 | switch v := authMethod.Method.(type) { 181 | case *plugin.AuthMethod_Scanqrcode: 182 | token, err = util.CheckAuthQrcode("alipan", v.Scanqrcode.QrcodeImageParam) 183 | if err != nil { 184 | return nil, err 185 | } 186 | if token == nil { 187 | return nil, nil 188 | } 189 | case *plugin.AuthMethod_Refresh: 190 | token := &plugin.Token{} 191 | err = token.UnmarshalVT(v.Refresh.AuthData.AuthDataBytes) 192 | if err != nil { 193 | return nil, err 194 | } 195 | refreshToken = token.RefreshToken 196 | case *plugin.AuthMethod_Callback: 197 | slog.Info("recv callback data", "callBackData", v.Callback.CallbackUrlData) 198 | token = &plugin.Token{} 199 | urlParse, err := url.Parse(v.Callback.CallbackUrlData) 200 | if err != nil { 201 | return nil, err 202 | } 203 | data, err := base64.URLEncoding.DecodeString(urlParse.Query().Get("token")) 204 | if err != nil { 205 | return nil, err 206 | } 207 | err = token.UnmarshalVT(data) 208 | if err != nil { 209 | return nil, err 210 | } 211 | default: 212 | return nil, fmt.Errorf("unsupport %+v", v) 213 | } 214 | if token == nil { 215 | token, err = util.GetAuthToken(&util.GetAuthTokenRequest{ 216 | Id: "alipan", 217 | Code: authCode, 218 | State: state, 219 | RefreshToken: refreshToken, 220 | }) 221 | if err != nil { 222 | slog.Error("authCode to access token failed", "err", err) 223 | return nil, err 224 | } 225 | } 226 | 227 | tokenBytes, err := token.MarshalVT() 228 | if err != nil { 229 | slog.Error("marshal token failed", "err", err) 230 | return nil, err 231 | } 232 | expireTime := time.Now().Add(time.Second * time.Duration(token.ExpiresIn-300)).Unix() 233 | authData = &plugin.AuthData{ 234 | AuthDataBytes: tokenBytes, 235 | AuthDataExpiredTime: uint64(expireTime), 236 | } 237 | slog.Info("get access token success") 238 | return authData, nil 239 | } 240 | 241 | // InitAuth implements IPlugin. 242 | func (p *PluginImpl) CheckAuthData(authDataBytes []byte) error { 243 | token := &plugin.Token{} 244 | err := token.UnmarshalVT(authDataBytes) 245 | if err != nil { 246 | return err 247 | } 248 | p.token = token 249 | resp := &UserInfoResponse{} 250 | err = p.send(http.MethodGet, "/oauth/users/info", nil, resp) 251 | if err != nil { 252 | return err 253 | } 254 | p.userInfo = resp 255 | p.getDriverInfoResponse = &UserGetDriverInfoResponse{} 256 | err = p.send(http.MethodPost, "/adrive/v1.0/user/getDriveInfo", nil, p.getDriverInfoResponse) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | return nil 262 | } 263 | 264 | // AuthId implements IPlugin. 265 | func (p *PluginImpl) PluginAuthId() (string, error) { 266 | if p.userInfo == nil { 267 | return "", fmt.Errorf("can get user info") 268 | } 269 | return p.userInfo.Id, nil 270 | } 271 | 272 | func (d *PluginImpl) getDriverPath(path string) (string, string, error) { 273 | path = filepath.Clean(path) 274 | var driverId string 275 | if strings.HasPrefix(path, "/资源库") { 276 | path, _ = strings.CutPrefix(path, "/资源库") 277 | driverId = d.getDriverInfoResponse.ResourceDriverId 278 | } else if strings.HasPrefix(path, "/备份盘") { 279 | path, _ = strings.CutPrefix(path, "/备份盘") 280 | driverId = d.getDriverInfoResponse.BackupDriverId 281 | } else { 282 | slog.Error("valid path", "path", path) 283 | return "", "", os.ErrNotExist 284 | } 285 | if path == "" { 286 | path = "/" 287 | } 288 | return driverId, path, nil 289 | } 290 | 291 | var ( 292 | pageMarker sync.Map 293 | ) 294 | 295 | // GetDirEntry implements IPlugin. 296 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 297 | dirEntry := &plugin.DirEntry{ 298 | FileEntries: []*plugin.FileEntry{}, 299 | PageSize: 100, 300 | } 301 | if req.PageSize == 0 { 302 | req.PageSize = dirEntry.PageSize 303 | } 304 | if req.Path == "/" { 305 | if p.getDriverInfoResponse.ResourceDriverId != "" { 306 | dirEntry.FileEntries = append(dirEntry.FileEntries, &plugin.FileEntry{ 307 | Name: "资源库", 308 | FileType: plugin.FileEntry_FileTypeDir, 309 | }) 310 | } 311 | if p.getDriverInfoResponse.BackupDriverId != "" { 312 | dirEntry.FileEntries = append(dirEntry.FileEntries, &plugin.FileEntry{ 313 | Name: "备份盘", 314 | FileType: plugin.FileEntry_FileTypeDir, 315 | }) 316 | } 317 | return dirEntry, nil 318 | } 319 | driverId, path, err := p.getDriverPath(req.Path) 320 | if err != nil { 321 | slog.Error("getDriver failed", "err", err) 322 | return nil, err 323 | } 324 | parentFileId := "" 325 | if path == "/" { 326 | parentFileId = "root" 327 | } else { 328 | fileEntry := &FileEntry{} 329 | if req.FileEntry != nil && req.FileEntry.RawData != nil { 330 | err = json.Unmarshal(req.FileEntry.RawData, fileEntry) 331 | if err != nil { 332 | slog.Error("unmarshal failed", "rawData", string(req.FileEntry.RawData), "err", err) 333 | return nil, err 334 | } 335 | parentFileId = fileEntry.FileId 336 | } 337 | if parentFileId == "" { 338 | fileEntry, err = p.getFileEntryInfoByPath(driverId, path) 339 | if err != nil { 340 | return nil, err 341 | } 342 | 343 | parentFileId = fileEntry.FileId 344 | } 345 | if parentFileId == "" { 346 | return nil, errors.New("parent file id is empty") 347 | } 348 | parentFileId = fileEntry.FileId 349 | } 350 | if req.Page > 1 { 351 | // page is empty 352 | return dirEntry, nil 353 | } 354 | 355 | openFileReq := &OpenFileListRequest{ 356 | DriveId: driverId, 357 | Limit: int(req.PageSize), 358 | OrderBy: "name_enhanced", 359 | ParentFileId: parentFileId, 360 | Category: "", 361 | Marker: req.DirPageKey, 362 | } 363 | 364 | openFileRsp := &OpenFileListResponse{} 365 | err = p.send(http.MethodPost, "/adrive/v1.0/openFile/list", openFileReq, openFileRsp) 366 | if err != nil { 367 | return nil, err 368 | } 369 | for _, item := range openFileRsp.Items { 370 | fileType := plugin.FileEntry_FileTypeFile 371 | if item.Type == "folder" { 372 | fileType = plugin.FileEntry_FileTypeDir 373 | } 374 | fileEntry := &plugin.FileEntry{ 375 | Name: item.Name, 376 | FileType: fileType, 377 | Size: item.Size, 378 | CreatedTime: uint64(item.CreatedTime.Unix()), 379 | ModifiedTime: uint64(item.UpdatedTime.Unix()), 380 | AccessedTime: uint64(item.UpdatedTime.Unix()), 381 | } 382 | itemBytes, err := json.Marshal(item) 383 | if err == nil { 384 | fileEntry.RawData = itemBytes 385 | } 386 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 387 | } 388 | dirEntry.DirPageKey = openFileRsp.NextMarker 389 | return dirEntry, nil 390 | } 391 | 392 | func (p *PluginImpl) getFileEntryInfoByPath(driverId, path string) (*FileEntry, error) { 393 | 394 | rsp := &OpenFilegetbypathResponse{ 395 | FileEntry: &FileEntry{}, 396 | } 397 | req := &OpenFilegetbypathRequest{ 398 | DriveId: driverId, 399 | FilePath: path, 400 | } 401 | 402 | err := p.send(http.MethodPost, "/adrive/v1.0/openFile/get_by_path", req, rsp) 403 | if err != nil { 404 | return nil, err 405 | } 406 | slog.Info("getFileEntryInfoByPath", "resp", rsp) 407 | return rsp.FileEntry, nil 408 | } 409 | 410 | // GetFileResource implements IPlugin. 411 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 412 | driverId, path, err := p.getDriverPath(req.FilePath) 413 | if err != nil { 414 | return nil, err 415 | } 416 | var fileId string 417 | if req.FileEntry != nil && req.FileEntry.RawData != nil { 418 | fileEntry := FileEntry{} 419 | err = json.Unmarshal(req.FileEntry.RawData, &fileEntry) 420 | if err != nil { 421 | return nil, err 422 | } 423 | fileId = fileEntry.FileId 424 | } 425 | if fileId == "" { 426 | fileEntry, err := p.getFileEntryInfoByPath(driverId, path) 427 | if err != nil { 428 | return nil, err 429 | } 430 | fileId = fileEntry.FileId 431 | } 432 | 433 | fileResource := &plugin.FileResource{ 434 | FileResourceData: []*plugin.FileResource_FileResourceData{}, 435 | } 436 | 437 | getFileDownloadReq := &OpenFilegetDownloadUrlRequest{ 438 | DriveId: driverId, 439 | FileId: fileId, 440 | ExpireSec: 900, 441 | } 442 | getFileDownloadResp := &OpenFilegetDownloadUrlResponse{} 443 | err = p.send(http.MethodPost, "/adrive/v1.0/openFile/getDownloadUrl", getFileDownloadReq, getFileDownloadResp) 444 | if err != nil { 445 | return nil, err 446 | } 447 | expireTime, err := time.Parse("2006-01-02T15:04:05Z", getFileDownloadResp.Expiration) 448 | if err == nil { 449 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 450 | Url: getFileDownloadResp.Url, 451 | ExpireTime: uint64(expireTime.Unix()), 452 | Resolution: plugin.FileResource_Original, 453 | ResourceType: plugin.FileResource_Video, 454 | }) 455 | } else { 456 | slog.Error("get download url failed", "err", err) 457 | } 458 | playReq := &OpenFileGetVideoPreviewPlayInfoRequest{ 459 | DriveId: driverId, 460 | FileId: fileId, 461 | Category: "live_transcoding", 462 | GetSubtitleInfo: true, 463 | TemplateId: "LD|SD|HD|FHD|QHD", 464 | UrlExpireSec: 4 * 60 * 50, 465 | } 466 | playRsp := &OpenFileGetVideoPreviewPlayInfoResponse{ 467 | VideoPreViewPlayInfo: &VideoPreViewPlayInfo{ 468 | LiveTranscodingTaskList: []*LiveTranscodingTask{}, 469 | LiveTranscodingSubtitleTaskList: []*LiveTranscodingSubtitleTask{}, 470 | }, 471 | } 472 | err = p.send(http.MethodPost, "/adrive/v1.0/openFile/getVideoPreviewPlayInfo", playReq, playRsp) 473 | if err == nil { 474 | for _, task := range playRsp.VideoPreViewPlayInfo.LiveTranscodingTaskList { 475 | if task.Status != "finished" { 476 | continue 477 | } 478 | var resolution plugin.FileResource_Resolution 479 | switch task.TemplateId { 480 | case "LD": 481 | resolution = plugin.FileResource_LD 482 | case "SD": 483 | resolution = plugin.FileResource_SD 484 | case "HD": 485 | resolution = plugin.FileResource_HD 486 | case "FHD": 487 | resolution = plugin.FileResource_FHD 488 | case "QHD": 489 | resolution = plugin.FileResource_QHD 490 | default: 491 | continue 492 | } 493 | if task.Url == "" { 494 | continue 495 | } 496 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 497 | Url: task.Url, 498 | ExpireTime: uint64(time.Now().Add(time.Second * 4 * 60 * 50).Unix()), 499 | ResourceType: plugin.FileResource_Video, 500 | Resolution: resolution, 501 | }) 502 | } 503 | for _, task := range playRsp.VideoPreViewPlayInfo.LiveTranscodingSubtitleTaskList { 504 | if task.Status != "finished" { 505 | continue 506 | } 507 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 508 | Url: task.Url, 509 | Title: task.Language, 510 | ResourceType: plugin.FileResource_Subtitle, 511 | }) 512 | } 513 | } else { 514 | slog.Error("get video preview play info failed", "err", err) 515 | } 516 | return fileResource, nil 517 | } 518 | -------------------------------------------------------------------------------- /alipan/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestPluginImpl(t *testing.T) { 11 | u := "mediagate://alipan/?code=bcb52515540f43fba4d791e6860943ad&state=1741361519159140" 12 | uuu, err := url.Parse(u) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | t.Logf("%+v", uuu.Query().Get("code")) 17 | 18 | t.Log(strings.CutPrefix("/资源库/Home/xxx/xxx", "/资源库")) 19 | 20 | // "2025-04-11T02:45:56.240Z" 21 | t.Log(time.Parse("2006-01-02T15:04:05Z", "2025-04-11T02:45:56.240Z")) 22 | } 23 | -------------------------------------------------------------------------------- /alist/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /alist/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /alist/alist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/alist/alist.png -------------------------------------------------------------------------------- /alist/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type AuthLogin struct { 6 | Username string `json:"username"` 7 | Password string `json:"password"` 8 | } 9 | 10 | type Response struct { 11 | Code int `json:"code"` 12 | Message string `json:"message"` 13 | Data any `json:"data"` 14 | } 15 | 16 | type TokenData struct { 17 | Token string `json:"token"` 18 | } 19 | 20 | type FsListReq struct { 21 | Page int64 `json:"page"` 22 | Password string `json:"password"` 23 | Path string `json:"path"` 24 | PerPage int64 `json:"per_page"` 25 | Refresh bool `json:"refresh"` 26 | } 27 | type FsListResp struct { 28 | Total int `json:"total"` 29 | Contents []Content `json:"content"` 30 | } 31 | 32 | type Content struct { 33 | Name string `json:"name"` 34 | Size int64 `json:"size"` 35 | IsDir bool `json:"is_dir"` 36 | Modified time.Time `json:"modified"` 37 | Created time.Time `json:"created"` 38 | 39 | RawUrl string `json:"raw_url"` 40 | } 41 | 42 | type FsGetReq struct { 43 | Path string 44 | } 45 | type FsGetResp struct { 46 | Path string 47 | } 48 | -------------------------------------------------------------------------------- /alist/plugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by plugin_api. DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /alist/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "alist" 2 | Name = "Alist" 3 | desc = "support alist" 4 | icon = "alist.png" 5 | author = ["labulakalia@gmail.com"] 6 | version = "v0.0.2" 7 | changelog = ["fix list dir failed"] 8 | -------------------------------------------------------------------------------- /alist/plugin_impl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net/http" 11 | "time" 12 | 13 | "github.com/medianexapp/plugin_api/httpclient" 14 | 15 | "github.com/medianexapp/plugin_api/plugin" 16 | 17 | _ "github.com/labulakalia/wazero_net/wasi/http" // if you need http import this 18 | _ "github.com/labulakalia/wazero_net/wasi/net" // if you need net.Conn import this 19 | ) 20 | 21 | type PluginImpl struct { 22 | authData *AuthData 23 | client *httpclient.Client 24 | } 25 | 26 | type AuthData struct { 27 | Addr string 28 | Username string 29 | Password string 30 | TokenExpired int64 31 | Token string 32 | } 33 | 34 | func NewPluginImpl() *PluginImpl { 35 | slog.SetLogLoggerLevel(slog.LevelDebug) 36 | return &PluginImpl{ 37 | authData: &AuthData{}, 38 | client: httpclient.NewClient(), 39 | } 40 | } 41 | 42 | // Id implements IPlugin. 43 | func (p *PluginImpl) PluginId() (string, error) { 44 | return "alist", nil 45 | } 46 | 47 | // GetAuth return how to auth 48 | // 1.FormData input data 49 | // 2.Callback use url callback auth,like oauth 50 | // 3.Scanqrcode,return qrcode image to auth 51 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 52 | auth := &plugin.Auth{ 53 | AuthMethods: []*plugin.AuthMethod{ 54 | { 55 | Method: &plugin.AuthMethod_Formdata{ 56 | Formdata: &plugin.Formdata{ 57 | FormItems: []*plugin.Formdata_FormItem{ 58 | { 59 | Name: "Addr", 60 | Value: plugin.String("http://127.0.0.1:5244"), 61 | }, 62 | { 63 | Name: "Username", 64 | Value: plugin.String(""), 65 | }, 66 | { 67 | Name: "Password", 68 | Value: plugin.ObscureString(""), 69 | }, 70 | { 71 | Name: "Default Token Expired(H)", 72 | Value: plugin.Int64(48), 73 | }, 74 | }, 75 | }, 76 | }, 77 | }, 78 | }, 79 | } 80 | return auth, nil 81 | } 82 | 83 | func (p *PluginImpl) request(method string, uri string, reqData, respData any) error { 84 | var body io.Reader 85 | if reqData != nil { 86 | data, err := json.Marshal(reqData) 87 | if err != nil { 88 | return err 89 | } 90 | fmt.Println(string(data)) 91 | body = bytes.NewReader(data) 92 | } 93 | slog.Debug("alist request", "req", reqData, "url", fmt.Sprintf("%s%s", p.authData.Addr, uri)) 94 | req, err := http.NewRequest(method, fmt.Sprintf("%s%s", p.authData.Addr, uri), body) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | if p.authData.Token != "" { 100 | req.Header.Set("Authorization", p.authData.Token) 101 | } 102 | req.Header.Add("Content-Type", "application/json") 103 | httpResp, err := p.client.Do(req) 104 | if err != nil { 105 | return err 106 | } 107 | defer httpResp.Body.Close() 108 | 109 | data, err := io.ReadAll(httpResp.Body) 110 | if err != nil { 111 | return err 112 | } 113 | resp := &Response{ 114 | Data: respData, 115 | } 116 | err = json.Unmarshal(data, resp) 117 | if err != nil { 118 | return err 119 | } 120 | if resp.Code != 200 { 121 | return fmt.Errorf("%s", resp.Message) 122 | } 123 | return nil 124 | } 125 | 126 | // CheckAuthMethod check auth is finished and return authDataBytes and authData's expired time 127 | // if authmethod's type is *plugin.AuthMethod_Refresh,you need to refresh token 128 | // assert authMethod.Method's type to check auth is finished,return auth data and expired time if authed 129 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (*plugin.AuthData, error) { 130 | slog.Debug("CheckAuthMethod", "authMethod", authMethod) 131 | switch data := authMethod.Method.(type) { 132 | case *plugin.AuthMethod_Refresh: 133 | case *plugin.AuthMethod_Formdata: 134 | forms := data.Formdata.FormItems 135 | p.authData.Addr = forms[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value 136 | p.authData.Username = forms[1].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value 137 | p.authData.Password = forms[2].Value.(*plugin.Formdata_FormItem_ObscureStringValue).ObscureStringValue.Value 138 | p.authData.TokenExpired = forms[3].Value.(*plugin.Formdata_FormItem_Int64Value).Int64Value.Value 139 | } 140 | authResp := &TokenData{} 141 | err := p.request(http.MethodPost, "/api/auth/login", &AuthLogin{ 142 | Username: p.authData.Username, 143 | Password: p.authData.Password, 144 | }, authResp) 145 | if err != nil { 146 | return nil, err 147 | } 148 | p.authData.Token = authResp.Token 149 | authData, err := json.Marshal(p.authData) 150 | if err != nil { 151 | return nil, err 152 | } 153 | return &plugin.AuthData{ 154 | AuthDataBytes: authData, 155 | AuthDataExpiredTime: uint64(time.Hour * time.Duration(p.authData.TokenExpired)), 156 | }, nil 157 | } 158 | 159 | // CheckAuthData use authDataBytes to uath 160 | // you must store auth data to *PluginImpl 161 | func (p *PluginImpl) CheckAuthData(authDataBytes []byte) error { 162 | slog.Debug("CheckAuthData", "authDataBytes", authDataBytes) 163 | err := json.Unmarshal(authDataBytes, p.authData) 164 | if err != nil { 165 | return err 166 | } 167 | err = p.request(http.MethodGet, "/api/me", nil, nil) 168 | if err != nil { 169 | return err 170 | } 171 | return err 172 | } 173 | 174 | // PluginAuthId implements IPlugin. 175 | // plugin auth id,you can generate id by md5 or sha 176 | func (p *PluginImpl) PluginAuthId() (string, error) { 177 | data, err := json.Marshal(p.authData) 178 | if err != nil { 179 | return "", err 180 | } 181 | return fmt.Sprintf("%x", md5.Sum(data)), nil 182 | } 183 | 184 | // GetDirEntry implements IPlugin. 185 | // return dir file entry 186 | // save your driver file raw data to FileEntry.RawData,you can get it after GetDirEntry and GetFileResource request 187 | // default page_size if 100,if this not for you,change is on DirEntry.PageSize,will use new PageSize for next request 188 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 189 | slog.Debug("GetDirEntry", "req", req) 190 | fsResp := &FsListResp{ 191 | Contents: []Content{}, 192 | } 193 | fsReq := &FsListReq{ 194 | Path: req.Path, 195 | Page: int64(req.Page), 196 | PerPage: int64(req.PageSize), 197 | Refresh: false, 198 | } 199 | err := p.request(http.MethodPost, "/api/fs/list", fsReq, fsResp) 200 | if err != nil { 201 | return nil, err 202 | } 203 | dirEntry := plugin.DirEntry{ 204 | FileEntries: []*plugin.FileEntry{}, 205 | } 206 | for _, content := range fsResp.Contents { 207 | fileEntry := &plugin.FileEntry{ 208 | Name: content.Name, 209 | ModifiedTime: uint64(content.Modified.Unix()), 210 | AccessedTime: uint64(content.Modified.Unix()), 211 | CreatedTime: uint64(content.Created.Unix()), 212 | Size: uint64(content.Size), 213 | } 214 | if content.IsDir { 215 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 216 | } else { 217 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 218 | } 219 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 220 | } 221 | return &dirEntry, nil 222 | } 223 | 224 | // GetFileResource implements IPlugin. 225 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 226 | slog.Debug("GetFileResource", "req", req) 227 | fsGetReq := &FsGetReq{ 228 | Path: req.FilePath, 229 | } 230 | content := &Content{} 231 | err := p.request(http.MethodPost, "/api/fs/get", fsGetReq, content) 232 | if err != nil { 233 | return nil, err 234 | } 235 | return &plugin.FileResource{ 236 | FileResourceData: []*plugin.FileResource_FileResourceData{ 237 | { 238 | Url: content.RawUrl, 239 | ResourceType: plugin.FileResource_Video, 240 | Resolution: plugin.FileResource_Original, 241 | }, 242 | }, 243 | }, nil 244 | } 245 | -------------------------------------------------------------------------------- /alist/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/medianexapp/plugin_api/plugin" 7 | ) 8 | 9 | func TestPluginImpl(t *testing.T) { 10 | p := NewPluginImpl() 11 | auth, _ := p.GetAuth() 12 | method := auth.AuthMethods[0].Method 13 | authFormData := method.(*plugin.AuthMethod_Formdata) 14 | authFormData.Formdata.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value = "http://127.0.0.1:5244" 15 | authFormData.Formdata.FormItems[1].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value = "admin" 16 | authFormData.Formdata.FormItems[2].Value.(*plugin.Formdata_FormItem_ObscureStringValue).ObscureStringValue.Value = "password" 17 | authFormData.Formdata.FormItems[3].Value.(*plugin.Formdata_FormItem_Int64Value).Int64Value.Value = 48 18 | 19 | authData, err := p.CheckAuthMethod(&plugin.AuthMethod{ 20 | Method: authFormData, 21 | }) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | err = p.CheckAuthData(authData.AuthDataBytes) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | resp, err := p.GetDirEntry(&plugin.GetDirEntryRequest{ 30 | Path: "/", 31 | Page: 1, 32 | PageSize: 100, 33 | }) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | t.Log(resp.FileEntries) 38 | 39 | fileResource, err := p.GetFileResource(&plugin.GetFileResourceRequest{ 40 | FilePath: "/tianyi/我的视频/绝对权力[简繁英字幕].Absolute.Power.1997.EUR.1080p.BluRay.x265.10bit.DTS-SONYHD/Absolute.Power.1997.EUR.1080p.BluRay.x265.10bit.DTS-SONYHD.mkv", 41 | }) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | t.Logf("get file fileResource %+v", fileResource.FileResourceData[0]) 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /baidupan/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /baidupan/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /baidupan/baidupan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/baidupan/baidupan.png -------------------------------------------------------------------------------- /baidupan/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | BaiduPanURL = "https://pan.baidu.com" 5 | ) 6 | 7 | type QrcodeData struct { 8 | DeviceCode string `json:"device_code"` 9 | UserCode string `json:"user_code"` 10 | VerificationUrl string `json:"verification_url"` 11 | QrcodeURL string `json:"qrcode_url"` 12 | ExpiresIn uint64 `json:"expires_in"` 13 | Interval uint64 `json:"interval"` 14 | } 15 | 16 | type Response struct { 17 | Errno int `json:"errno"` 18 | ErrMsg string `json:"errmsg"` 19 | RequestId any `json:"request_id"` 20 | } 21 | 22 | // / 23 | type UserInfo struct { 24 | BaiduName string `json:"baidu_name"` 25 | NetdiskName string `json:"netdisk_name"` 26 | Uk uint64 `json:"uk"` 27 | VipType uint64 `json:"vip_type"` 28 | } 29 | 30 | type FileListItem struct { 31 | FsId uint64 `json:"fs_id"` 32 | Path string `json:"path"` 33 | ServerFilename string `json:"server_filename"` 34 | Size uint64 `json:"size"` 35 | ServerMtime uint64 `json:"server_mtime"` 36 | ServerAtime uint64 `json:"server_atime"` 37 | ServerCtime uint64 `json:"server_ctime"` 38 | IsDir uint64 `json:"isdir"` 39 | Category uint64 `json:"category"` 40 | } 41 | 42 | type FileListResponse struct { 43 | List []*FileListItem `json:"list"` 44 | } 45 | 46 | type FileMetasRequest struct { 47 | FsIds []uint64 `query:"fsids"` 48 | Dlink int `query:"dlink"` // 1 49 | Thumb int `query:"thumb"` // 1 50 | NeedMedia int `query:"needmedia"` // 1 51 | Detail int `query:"detail"` // 1 52 | } 53 | 54 | type FileMetaItem struct { 55 | Category uint64 `json:"category"` 56 | DateTaken uint64 `json:"date_taken"` 57 | Dlink string `json:"dlink"` // expire 8H 58 | Filename string `json:"filename"` 59 | FsID int64 `json:"fs_id"` 60 | Height uint64 `json:"height"` 61 | Isdir uint64 `json:"isdir"` 62 | Md5 string `json:"md5"` 63 | OperID uint64 `json:"oper_id"` 64 | Path string `json:"path"` 65 | ServerCtime uint64 `json:"server_ctime"` 66 | ServerMtime uint64 `json:"server_mtime"` 67 | Size uint64 `json:"size"` 68 | Thumbs struct { 69 | Icon string `json:"icon"` 70 | URL1 string `json:"url1"` 71 | URL2 string `json:"url2"` 72 | URL3 string `json:"url3"` 73 | } `json:"thumbs"` 74 | Width uint64 `json:"width"` 75 | } 76 | 77 | type FileMetasResponse struct { 78 | List []*FileMetaItem `json:"list"` 79 | } 80 | -------------------------------------------------------------------------------- /baidupan/plugin.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /baidupan/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "baidupan" 2 | name = "BaiduPan" 3 | desc = "baidu pan driver plugin" 4 | icon = "baidupan.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.2" 7 | changelog = ["fix unmarshal oper id failed"] 8 | -------------------------------------------------------------------------------- /baidupan/plugin_impl.go: -------------------------------------------------------------------------------- 1 | //go:build wasip1 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/md5" 7 | "encoding/base64" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "log/slog" 13 | "net/http" 14 | "net/url" 15 | "plugins/util" 16 | "time" 17 | 18 | netutil "github.com/labulakalia/wazero_net/util" 19 | _ "github.com/labulakalia/wazero_net/wasi/http" 20 | "github.com/medianexapp/plugin_api/httpclient" 21 | "github.com/medianexapp/plugin_api/plugin" 22 | "github.com/medianexapp/plugin_api/ratelimit" 23 | ) 24 | 25 | /* 26 | NOTE: net and http use package 27 | "github.com/labulakalia/wazero_net/wasi/http" 28 | "github.com/labulakalia/wazero_net/wasi/net" 29 | */ 30 | 31 | type PluginImpl struct { 32 | client *httpclient.Client 33 | token *plugin.Token 34 | userInfo *UserInfo 35 | ratelimit *ratelimit.RateLimit 36 | } 37 | 38 | func NewPluginImpl() *PluginImpl { 39 | return &PluginImpl{ 40 | client: httpclient.NewClient(), 41 | ratelimit: ratelimit.New(map[string]ratelimit.LimitConfig{}), 42 | } 43 | } 44 | 45 | // Id implements IPlugin. 46 | func (p *PluginImpl) PluginId() (string, error) { 47 | return "baidupan", nil 48 | } 49 | 50 | // GetAuthe implements IPlugin. 51 | // Note: not store var in GetAuth 52 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 53 | auth := &plugin.Auth{ 54 | AuthMethods: []*plugin.AuthMethod{}, 55 | } 56 | authUrl := util.GetAuthAddr("baidupan") 57 | callback := &plugin.AuthMethod_Callback{ 58 | Callback: &plugin.Callback{ 59 | CallbackUrl: authUrl, 60 | }, 61 | } 62 | 63 | auth.AuthMethods = append(auth.AuthMethods, &plugin.AuthMethod{ 64 | Method: callback, 65 | }) 66 | 67 | res, err := util.GetAuthQrcode("baidupan") 68 | if err != nil { 69 | slog.Error("get auth qrcode failed", "err", err) 70 | return nil, err 71 | } 72 | qrCode := QrcodeData{} 73 | err = json.Unmarshal(res, &qrCode) 74 | if err != nil { 75 | slog.Error("unmarshal auth qrcode failed", "err", err) 76 | return nil, err 77 | } 78 | qrResp, err := p.client.Get(qrCode.QrcodeURL) 79 | if err != nil { 80 | return nil, err 81 | } 82 | qrRespBytes, err := io.ReadAll(qrResp.Body) 83 | if err != nil { 84 | return nil, err 85 | } 86 | if qrResp.StatusCode != http.StatusOK { 87 | return nil, errors.New(netutil.BytesToString(qrRespBytes)) 88 | } 89 | authQrcode := &plugin.AuthMethod_Scanqrcode{ 90 | Scanqrcode: &plugin.Scanqrcode{ 91 | QrcodeImage: qrRespBytes, 92 | QrcodeImageParam: qrCode.DeviceCode, 93 | QrcodeExpireTime: uint64(time.Now().Unix()) + uint64(qrCode.ExpiresIn), 94 | }, 95 | } 96 | 97 | auth.AuthMethods = append(auth.AuthMethods, &plugin.AuthMethod{ 98 | Method: authQrcode, 99 | }) 100 | return auth, nil 101 | } 102 | 103 | // CheckAuth implements IPlugin. 104 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 105 | var ( 106 | token *plugin.Token 107 | authCode string 108 | refreshToken string 109 | state string 110 | ) 111 | switch v := authMethod.Method.(type) { 112 | case *plugin.AuthMethod_Scanqrcode: 113 | time.Sleep(time.Second * 3) 114 | token, err = util.CheckAuthQrcode("baidupan", v.Scanqrcode.QrcodeImageParam) 115 | if err != nil { 116 | return nil, err 117 | } 118 | if token == nil { 119 | return nil, nil 120 | } 121 | case *plugin.AuthMethod_Refresh: 122 | token := &plugin.Token{} 123 | err = token.UnmarshalVT(v.Refresh.AuthData.AuthDataBytes) 124 | if err != nil { 125 | return nil, err 126 | } 127 | refreshToken = token.RefreshToken 128 | case *plugin.AuthMethod_Callback: 129 | slog.Info("recv callback data", "callBackData", v.Callback.CallbackUrlData) 130 | token = &plugin.Token{} 131 | urlParse, err := url.Parse(v.Callback.CallbackUrlData) 132 | if err != nil { 133 | return nil, err 134 | } 135 | data, err := base64.URLEncoding.DecodeString(urlParse.Query().Get("token")) 136 | if err != nil { 137 | return nil, err 138 | } 139 | err = token.UnmarshalVT(data) 140 | if err != nil { 141 | return nil, err 142 | } 143 | default: 144 | return nil, fmt.Errorf("unsupport %+v", v) 145 | } 146 | if token == nil { 147 | token, err = util.GetAuthToken(&util.GetAuthTokenRequest{ 148 | Id: "baidupan", 149 | Code: authCode, 150 | State: state, 151 | RefreshToken: refreshToken, 152 | }) 153 | if err != nil { 154 | slog.Error("authCode to access token failed", "err", err) 155 | return nil, err 156 | } 157 | } 158 | 159 | tokenBytes, err := token.MarshalVT() 160 | if err != nil { 161 | slog.Error("marshal token failed", "err", err) 162 | return nil, err 163 | } 164 | expireTime := time.Now().Add(time.Second * time.Duration(token.ExpiresIn-300)).Unix() 165 | authData = &plugin.AuthData{ 166 | AuthDataBytes: tokenBytes, 167 | AuthDataExpiredTime: uint64(expireTime), 168 | } 169 | slog.Info("get access token success") 170 | return authData, nil 171 | } 172 | 173 | func (p *PluginImpl) sendData(_ string, uri string, u url.Values, resp any) error { 174 | u.Add("access_token", p.token.AccessToken) 175 | reqUrl := fmt.Sprintf("%s%s?%s", BaiduPanURL, uri, u.Encode()) 176 | reqResp, err := p.client.Get(reqUrl) 177 | if err != nil { 178 | slog.Error("request url failed", "req", reqUrl) 179 | return err 180 | } 181 | defer reqResp.Body.Close() 182 | body, err := io.ReadAll(reqResp.Body) 183 | if err != nil { 184 | slog.Error("read response body failed", "err", err) 185 | return err 186 | } 187 | respData := &Response{} 188 | err = json.Unmarshal(body, respData) 189 | if err != nil { 190 | slog.Error("unmarshal response failed", "err", err) 191 | return err 192 | } 193 | if respData.Errno != 0 { 194 | slog.Error("request failed", "errno", respData.Errno, "errmsg", respData.ErrMsg) 195 | return errors.New(respData.ErrMsg) 196 | } 197 | err = json.Unmarshal(body, resp) 198 | if err != nil { 199 | slog.Error("unmarshal response failed", "err", err) 200 | return err 201 | } 202 | return nil 203 | } 204 | 205 | // InitAuth implements IPlugin. 206 | func (p *PluginImpl) CheckAuthData(authDataBytes []byte) error { 207 | token := &plugin.Token{} 208 | err := token.UnmarshalVT(authDataBytes) 209 | if err != nil { 210 | return err 211 | } 212 | p.token = token 213 | userInfo := &UserInfo{} 214 | u := url.Values{} 215 | u.Add("method", "uinfo") 216 | err = p.sendData(http.MethodGet, "/rest/2.0/xpan/nas", u, userInfo) 217 | if err != nil { 218 | return err 219 | } 220 | p.userInfo = userInfo 221 | return nil 222 | } 223 | 224 | // AuthId implements IPlugin. 225 | func (p *PluginImpl) PluginAuthId() (string, error) { 226 | return fmt.Sprintf("%x", md5.Sum([]byte(p.userInfo.NetdiskName))), nil 227 | } 228 | 229 | // GetDirEntry implements IPlugin. 230 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 231 | u := url.Values{} 232 | u.Add("method", "list") 233 | u.Add("dir", req.Path) 234 | u.Add("order", "name") 235 | u.Add("desc", "1") 236 | u.Add("start", fmt.Sprint((req.Page-1)*req.PageSize)) 237 | resp := &FileListResponse{ 238 | List: []*FileListItem{}, 239 | } 240 | err := p.sendData(http.MethodGet, "/rest/2.0/xpan/file", u, resp) 241 | if err != nil { 242 | slog.Error("list file failed", "err", err) 243 | return nil, err 244 | } 245 | dirEntry := plugin.DirEntry{ 246 | FileEntries: []*plugin.FileEntry{}, 247 | } 248 | for _, fileItem := range resp.List { 249 | entry := &plugin.FileEntry{ 250 | Name: fileItem.ServerFilename, 251 | Size: fileItem.Size, 252 | CreatedTime: fileItem.ServerCtime, 253 | ModifiedTime: fileItem.ServerMtime, 254 | AccessedTime: fileItem.ServerAtime, 255 | FileType: plugin.FileEntry_FileTypeFile, 256 | } 257 | itemBytes, err := json.Marshal(fileItem) 258 | if err == nil { 259 | entry.RawData = itemBytes 260 | } 261 | if fileItem.IsDir == 1 { 262 | entry.FileType = plugin.FileEntry_FileTypeDir 263 | } 264 | dirEntry.FileEntries = append(dirEntry.FileEntries, entry) 265 | } 266 | return &dirEntry, nil 267 | } 268 | 269 | // GetFileResource implements IPlugin. 270 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 271 | fileItem := &FileListItem{} 272 | err := json.Unmarshal(req.FileEntry.RawData, fileItem) 273 | if err != nil { 274 | return nil, err 275 | } 276 | 277 | fileMetaResp := &FileMetasResponse{ 278 | List: []*FileMetaItem{}, 279 | } 280 | u := url.Values{} 281 | u.Add("method", "filemetas") 282 | u.Add("fsids", fmt.Sprintf("[%d]", fileItem.FsId)) 283 | u.Add("dlink", "1") 284 | err = p.sendData(http.MethodGet, "/rest/2.0/xpan/multimedia", u, fileMetaResp) 285 | if err != nil { 286 | return nil, err 287 | } 288 | if len(fileMetaResp.List) == 0 { 289 | return nil, fmt.Errorf("file %s not found", req.FilePath) 290 | } 291 | fileResource := &plugin.FileResource{ 292 | FileResourceData: []*plugin.FileResource_FileResourceData{ 293 | { 294 | Url: fmt.Sprintf("%s&access_token=%s", fileMetaResp.List[0].Dlink, p.token.AccessToken), 295 | ExpireTime: uint64(time.Now().Add(time.Hour * 8).Unix()), 296 | Resolution: plugin.FileResource_Original, 297 | Header: map[string]string{ 298 | "Host": "d.pcs.baidu.com", 299 | "User-Agent": "pan.baidu.com", 300 | }, 301 | }, 302 | }, 303 | } 304 | return fileResource, nil 305 | } 306 | -------------------------------------------------------------------------------- /baidupan/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestResponse(t *testing.T) { 9 | dd := `{ 10 | "avatar_url": "https://dss0.bdstatic.com/7Ls0a8Sm1A5BphGlnYG/sys/portrait/item/netdisk.1.3d20c095.phlucxvny00WCx9W4kLifw.jpg", 11 | "baidu_name": "百度用户A001", 12 | "errmsg": "succ", 13 | "errno": 0, 14 | "netdisk_name": "netdiskuser", 15 | "request_id": "674030589892501935", 16 | "uk": 208281036, 17 | "vip_type": 2 18 | }` 19 | type UserInfo struct { 20 | BaiduName string `json:"baidu_name"` 21 | Uk int `json:"uk"` 22 | } 23 | uinfo := UserInfo{} 24 | resp := Response{} 25 | t.Log(json.Unmarshal([]byte(dd), &resp)) 26 | t.Logf("%+v", uinfo) 27 | } 28 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; } 2 | 3 | function build() { 4 | version=`grep ^version $1/plugin.toml|awk -F'"' '{print $2}'` 5 | echo "$1 current version: "$version 6 | remoteVersion=`curl -s ${SERVER_ADDR}/api/get_plugin_version/$1` 7 | echo "$1 remote version: "$remoteVersion 8 | needUpload=0 9 | if [[ -z $remoteVersion ]];then 10 | echo "$1 remote version is empty" 11 | needUpload=1 12 | else 13 | if version_gt $version $remoteVersion; then 14 | echo "$1 $version is greater than $remoteVersion" 15 | needUpload=1 16 | else 17 | echo "$1 version not change" 18 | needUpload=0 19 | fi 20 | fi 21 | if [[ $needUpload -eq 1 ]];then 22 | echo "start build plugin $1" 23 | make -C $1 24 | curl -X POST "${SERVER_ADDR}/api/upload_plugin" \ 25 | -H 'Content-Type: application/zip' \ 26 | -H "SecretKey: ${SECRET_KEY}" \ 27 | --data-binary @"$1/dist/$1_$version.zip" 28 | fi 29 | } 30 | 31 | if [[ -z $SERVER_ADDR ]];then 32 | echo "vars SERVER_ADDR is empty" 33 | exit 1 34 | fi 35 | if [[ -z $SECRET_KEY ]];then 36 | echo "vars SECRET_KEY is empty" 37 | exit 1 38 | fi 39 | echo "{\"server_addr\": \"${SERVER_ADDR}\"}" > util/env.json 40 | 41 | for id in `ls -d */ | grep -v 'util' | grep -v smb | grep -v sftp |sed 's/\///g'` 42 | do 43 | build $id 44 | done 45 | echo "{}" > util/env.json 46 | -------------------------------------------------------------------------------- /ftp/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /ftp/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "Please install go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /ftp/ftp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/ftp/ftp.png -------------------------------------------------------------------------------- /ftp/plugin.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() { 15 | } 16 | -------------------------------------------------------------------------------- /ftp/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "ftp" 2 | name = "Ftp" 3 | desc = "ftp driver plugin" 4 | icon = "ftp.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.2" 7 | changelog = ["add reconnect sftp server"] 8 | -------------------------------------------------------------------------------- /ftp/plugin_impl.go: -------------------------------------------------------------------------------- 1 | //go:build wasip1 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/md5" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "net/netip" 11 | "strings" 12 | "time" 13 | 14 | "github.com/labulakalia/wazero_net/util" 15 | wasi_net "github.com/labulakalia/wazero_net/wasi/net" 16 | "github.com/medianexapp/ftp" 17 | "github.com/medianexapp/plugin_api/plugin" 18 | ) 19 | 20 | /* 21 | NOTE: net and http use package 22 | "github.com/labulakalia/wazero_net/wasi/http" 23 | "github.com/labulakalia/wazero_net/wasi/net" 24 | */ 25 | 26 | type PluginImpl struct { 27 | ftpAuth *ftpAuth 28 | ftpConn *ftp.ServerConn 29 | } 30 | 31 | func NewPluginImpl() *PluginImpl { 32 | ftpAuth := &ftpAuth{ 33 | Addr: plugin.String("127.0.0.1"), 34 | User: plugin.String(""), 35 | Password: plugin.ObscureString(""), 36 | } 37 | return &PluginImpl{ 38 | ftpAuth: ftpAuth, 39 | } 40 | } 41 | 42 | type ftpAuth struct { 43 | Addr *plugin.Formdata_FormItem_StringValue 44 | User *plugin.Formdata_FormItem_StringValue 45 | Password *plugin.Formdata_FormItem_ObscureStringValue 46 | } 47 | 48 | // Id implements IPlugin. 49 | func (p *PluginImpl) PluginId() (string, error) { 50 | return "ftp", nil 51 | } 52 | 53 | // GetAuthType implements IPlugin. 54 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 55 | 56 | formData := &plugin.AuthMethod_Formdata{ 57 | Formdata: &plugin.Formdata{ 58 | FormItems: []*plugin.Formdata_FormItem{ 59 | { 60 | Name: "Addr", 61 | Value: p.ftpAuth.Addr, 62 | }, 63 | { 64 | Name: "User", 65 | Value: p.ftpAuth.User, 66 | }, 67 | { 68 | Name: "Password", 69 | Value: p.ftpAuth.Password, 70 | }, 71 | }, 72 | }, 73 | } 74 | auth := &plugin.Auth{ 75 | AuthMethods: []*plugin.AuthMethod{&plugin.AuthMethod{Method: formData}}, 76 | } 77 | return auth, nil 78 | } 79 | 80 | // CheckAuthMethod implements IPlugin. 81 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 82 | // todo ftp over tls 83 | 84 | formDataBytes, err := authMethod.MarshalVT() 85 | if err != nil { 86 | return nil, err 87 | } 88 | return &plugin.AuthData{ 89 | AuthDataBytes: formDataBytes, 90 | }, nil 91 | } 92 | 93 | type ConnWrap struct { 94 | net.Conn 95 | readTimeout time.Duration 96 | } 97 | 98 | func (c *ConnWrap) Read(b []byte) (n int, err error) { 99 | err = c.Conn.SetReadDeadline(time.Now().Add(c.readTimeout)) 100 | if err != nil { 101 | return 0, err 102 | } 103 | 104 | return c.Conn.Read(b) 105 | } 106 | 107 | func NewWrapConn(conn net.Conn, readTimeout time.Duration) *ConnWrap { 108 | return &ConnWrap{ 109 | readTimeout: readTimeout, 110 | Conn: conn, 111 | } 112 | } 113 | 114 | func (p *PluginImpl) unmarshalFormData(formData *plugin.Formdata) { 115 | p.ftpAuth.Addr.StringValue = formData.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue 116 | p.ftpAuth.User.StringValue = formData.FormItems[1].Value.(*plugin.Formdata_FormItem_StringValue).StringValue 117 | p.ftpAuth.Password.ObscureStringValue = formData.FormItems[2].Value.(*plugin.Formdata_FormItem_ObscureStringValue).ObscureStringValue 118 | 119 | } 120 | 121 | func (p *PluginImpl) connectFtp() error { 122 | if p.ftpConn != nil { 123 | p.ftpConn.Logout() 124 | } 125 | addr := p.ftpAuth.Addr.StringValue.Value 126 | _, err := netip.ParseAddrPort(addr) 127 | if err != nil { 128 | addr = fmt.Sprintf("%s:%d", strings.TrimRight(addr, ":"), 21) 129 | } 130 | 131 | user := p.ftpAuth.User.StringValue.Value 132 | password := p.ftpAuth.Password.ObscureStringValue.Value 133 | if user == "" && password == "" { 134 | password = "anonymous" 135 | user = "anonymous" 136 | } 137 | 138 | ftpConn, err := ftp.Dial(addr, ftp.DialWithDialFunc(func(network, address string) (net.Conn, error) { 139 | conn, err := wasi_net.Dial(network, address) 140 | if err != nil { 141 | slog.Error("dial failed", "err", err) 142 | return nil, err 143 | } 144 | return NewWrapConn(conn, time.Second*5), nil 145 | })) 146 | if err != nil { 147 | return err 148 | } 149 | err = ftpConn.Login(user, password) 150 | if err != nil { 151 | slog.Error("ftp login failed", "addr", addr) 152 | return err 153 | } 154 | p.ftpConn = ftpConn 155 | return err 156 | } 157 | 158 | // InitAuth implements IPlugin. 159 | func (p *PluginImpl) CheckAuthData(AuthDataBytes []byte) error { 160 | authMethod := &plugin.AuthMethod{} 161 | err := authMethod.UnmarshalVT(AuthDataBytes) 162 | if err != nil { 163 | return err 164 | } 165 | formData := authMethod.Method.(*plugin.AuthMethod_Formdata) 166 | p.unmarshalFormData(formData.Formdata) 167 | err = p.connectFtp() 168 | if err != nil { 169 | return err 170 | } 171 | slog.Info("ftp login success", "addr", p.ftpAuth.Addr.StringValue.Value) 172 | return nil 173 | } 174 | 175 | // AuthId implements IPlugin. 176 | func (p *PluginImpl) PluginAuthId() (string, error) { 177 | id := fmt.Sprintf("%s%s%s", p.ftpAuth.Addr.StringValue.Value, p.ftpAuth.User.StringValue.Value, p.ftpAuth.Password.ObscureStringValue.Value) 178 | return fmt.Sprintf("%x", md5.Sum(util.StringToBytes(&id))), nil 179 | } 180 | 181 | // GetDirEntry implements IPlugin. 182 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 183 | dirPath := req.Path 184 | page := req.Page 185 | pageSize := req.PageSize 186 | 187 | var ( 188 | entries []*ftp.Entry 189 | err error 190 | ) 191 | for range 3 { 192 | entries, err = p.ftpConn.List(dirPath) 193 | if err != nil { 194 | slog.Error("list failed", "err", err) 195 | err = p.connectFtp() 196 | if err != nil { 197 | slog.Error("reconnect ftp failed", "err", err) 198 | } 199 | } else { 200 | break 201 | } 202 | } 203 | 204 | dirEntry := &plugin.DirEntry{ 205 | FileEntries: make([]*plugin.FileEntry, 0, pageSize), 206 | } 207 | 208 | start := int((page - 1) * pageSize) 209 | end := start + int(pageSize) 210 | 211 | if len(entries) <= start { 212 | return dirEntry, nil 213 | } else if len(entries) >= end { 214 | entries = entries[start:end] 215 | } else { 216 | entries = entries[start:] 217 | } 218 | 219 | for _, entry := range entries { 220 | if strings.HasPrefix(entry.Name, ".") { 221 | continue 222 | } 223 | fileEntry := &plugin.FileEntry{ 224 | Name: entry.Name, 225 | Size: entry.Size, 226 | CreatedTime: uint64(entry.Time.UnixMilli()), 227 | ModifiedTime: uint64(entry.Time.UnixMilli()), 228 | AccessedTime: uint64(entry.Time.UnixMilli()), 229 | } 230 | 231 | if entry.Type == ftp.EntryTypeFile { 232 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 233 | } else if entry.Type == ftp.EntryTypeFolder { 234 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 235 | } 236 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 237 | } 238 | return dirEntry, nil 239 | } 240 | 241 | // GetFileResource implements IPlugin. 242 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 243 | // url path ftp://[user[:password]@]server[:port]/path/to/remote/resource.mpeg 244 | var ( 245 | err error 246 | ) 247 | for range 3 { 248 | _, err = p.ftpConn.GetEntry(req.FilePath) 249 | if err != nil { 250 | slog.Error("list failed", "err", err) 251 | err = p.connectFtp() 252 | if err != nil { 253 | slog.Error("reconnect ftp failed", "err", err) 254 | 255 | } 256 | } else { 257 | break 258 | } 259 | } 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | userPass := "" 265 | if p.ftpAuth.User.StringValue.Value != "" || p.ftpAuth.Password.ObscureStringValue.Value != "" { 266 | userPass = fmt.Sprintf("%s:%s@", p.ftpAuth.User.StringValue.Value, p.ftpAuth.Password.ObscureStringValue.Value) 267 | } 268 | fileUrl := fmt.Sprintf("ftp://%s%s%s", userPass, p.ftpAuth.Addr.StringValue.Value, req.FilePath) 269 | return &plugin.FileResource{ 270 | FileResourceData: []*plugin.FileResource_FileResourceData{ 271 | { 272 | Url: fileUrl, 273 | Resolution: plugin.FileResource_Original, 274 | ResourceType: plugin.FileResource_Video, 275 | }, 276 | }, 277 | }, nil 278 | } 279 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module plugins 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/boombuler/barcode v1.0.2 7 | github.com/labulakalia/wazero_net v0.0.9-0.20250522104624-d43d2bec26eb 8 | github.com/medianexapp/ftp v0.0.0-20250425113218-131712bc06d6 9 | github.com/medianexapp/go-smb2 v0.0.0-20250425112922-92edacdefca5 10 | github.com/medianexapp/gowebdav v0.0.0-20250425112725-41a667437dfa 11 | github.com/medianexapp/plugin_api v0.0.25-0.20250522020710-a9284a5d036f 12 | github.com/medianexapp/sftp v1.13.10-0.20250425113120-4ffdd4c8163a 13 | golang.org/x/crypto v0.37.0 14 | ) 15 | 16 | require ( 17 | github.com/aperturerobotics/json-iterator-lite v1.0.0 // indirect 18 | github.com/aperturerobotics/protobuf-go-lite v0.9.1 // indirect 19 | github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc // indirect 20 | github.com/geoffgarside/ber v1.1.0 // indirect 21 | github.com/hashicorp/errwrap v1.0.0 // indirect 22 | github.com/hashicorp/go-multierror v1.1.1 // indirect 23 | github.com/hashicorp/go-uuid v1.0.3 // indirect 24 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 25 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 26 | github.com/jcmturner/gofork v1.7.6 // indirect 27 | github.com/jcmturner/goidentity/v6 v6.0.1 // indirect 28 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 29 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 30 | github.com/kr/fs v0.1.0 // indirect 31 | github.com/tetratelabs/wazero v1.9.1-0.20250414143203-0dea5d7ee1de // indirect 32 | golang.org/x/net v0.39.0 // indirect 33 | golang.org/x/sys v0.32.0 // indirect 34 | golang.org/x/time v0.11.0 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aperturerobotics/json-iterator-lite v1.0.0 h1:cihbrYWoK/S2RYXhJLpDZd+GUjVvFJN+D3w1VOqqHRI= 2 | github.com/aperturerobotics/json-iterator-lite v1.0.0/go.mod h1:snaApCEDtrHHP6UWSLKiYNOZU9A5NyzccKenx9oZEzg= 3 | github.com/aperturerobotics/protobuf-go-lite v0.9.1 h1:P1knXKnwLJpVE8fmeXYGckKu79IhqUvKRdCfJNR0MwQ= 4 | github.com/aperturerobotics/protobuf-go-lite v0.9.1/go.mod h1:fULrxQxEBWKQm7vvju9AfjTp9yfHoLgwMQWTiZQ2tg0= 5 | github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= 6 | github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 7 | github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc h1:0xCWmFKBmarCqqqLeM7jFBSw/Or81UEElFqO8MY+GDs= 8 | github.com/cloudsoda/sddl v0.0.0-20250224235906-926454e91efc/go.mod h1:uvR42Hb/t52HQd7x5/ZLzZEK8oihrFpgnodIJ1vte2E= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w= 13 | github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc= 14 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 15 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 16 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 17 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 18 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 19 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 20 | github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= 21 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 22 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 23 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 24 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 25 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 26 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 27 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 28 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 29 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 30 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 31 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 32 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 33 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 34 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 35 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 36 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 37 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 38 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 39 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 40 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 41 | github.com/labulakalia/wazero_net v0.0.9-0.20250522104624-d43d2bec26eb h1:/bYEv56DdCdIpgsnc/MpyHfEbSA8c2HLbmc6pOYhMyw= 42 | github.com/labulakalia/wazero_net v0.0.9-0.20250522104624-d43d2bec26eb/go.mod h1:cRw0flC7kvmKixxOscMnh/6Dc8YiDsIr+5YDKPafg6g= 43 | github.com/medianexapp/ftp v0.0.0-20250425113218-131712bc06d6 h1:q5B0eNkNJBzk/IC4o2PfJf2/eNw63xHSPYrZLs94uhA= 44 | github.com/medianexapp/ftp v0.0.0-20250425113218-131712bc06d6/go.mod h1:OJ5eKf4BN8PUG8L+ma7IOV+c772jYOnEpV9geeqGkic= 45 | github.com/medianexapp/go-smb2 v0.0.0-20250425112922-92edacdefca5 h1:rPMPOK4foIuVxiJJ0WnTcu9wvRFK4RTWyC9OK4TW3AI= 46 | github.com/medianexapp/go-smb2 v0.0.0-20250425112922-92edacdefca5/go.mod h1:j1QjZnoXKXrjkAOXeeWekE6ymSdCxWCHP4s4sF9PqPw= 47 | github.com/medianexapp/gowebdav v0.0.0-20250425112725-41a667437dfa h1:kKNtHeFLc8JRMc/Jol041krGY4JffYAWmGPzY8yZd5s= 48 | github.com/medianexapp/gowebdav v0.0.0-20250425112725-41a667437dfa/go.mod h1:UIu++AGXNUdvdqC2u746o78TGTmeG03n1TNPfLoLe6k= 49 | github.com/medianexapp/plugin_api v0.0.25-0.20250522020710-a9284a5d036f h1:dvAOYyfED/h1Qp1zeNOeRlePY7KmXUWpFrqXAsE9OOs= 50 | github.com/medianexapp/plugin_api v0.0.25-0.20250522020710-a9284a5d036f/go.mod h1:d+ioMKd/TYZl7axXX0PYM/2Ox1oyScIIvFrW/L6ymwQ= 51 | github.com/medianexapp/sftp v1.13.10-0.20250425113120-4ffdd4c8163a h1:a1AMBh4Glxjl8gb9JW1kJ1W92cgBBkLlnZHCOAPBjQg= 52 | github.com/medianexapp/sftp v1.13.10-0.20250425113120-4ffdd4c8163a/go.mod h1:BhBMCAsN8EaWmFxpa/Au+jX8fG8CLFKXs8LOjksAFH0= 53 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 54 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 56 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 57 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 58 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 59 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 60 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 61 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 62 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 63 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 64 | github.com/tetratelabs/wazero v1.9.1-0.20250414143203-0dea5d7ee1de h1:Ud2gtg9E/XuxoyIRJHnijTtwnrRQtt/DpY/PsiSi3dI= 65 | github.com/tetratelabs/wazero v1.9.1-0.20250414143203-0dea5d7ee1de/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 66 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 67 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 68 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 69 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 70 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 71 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 72 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 73 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 74 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 75 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 76 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 77 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 78 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 79 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 80 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 81 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 84 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 85 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 88 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 89 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 90 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 91 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 92 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 93 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 94 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 95 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 96 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 97 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 98 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 99 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 100 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 101 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 102 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 103 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 104 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 105 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 106 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 108 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 109 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 110 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 111 | -------------------------------------------------------------------------------- /local/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /local/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /local/local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/local/local.png -------------------------------------------------------------------------------- /local/plugin.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /local/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "local" 2 | name = "Local" 3 | desc = "local driver plugin" 4 | icon = "local.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.3" 7 | changelog = ["1.fix read dir failed on windows"] 8 | -------------------------------------------------------------------------------- /local/plugin_impl.go: -------------------------------------------------------------------------------- 1 | //go:build wasip1 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/md5" 7 | "fmt" 8 | "log/slog" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "syscall" 13 | 14 | "github.com/labulakalia/wazero_net/util" 15 | "github.com/medianexapp/plugin_api/plugin" 16 | ) 17 | 18 | /* 19 | NOTE: net and http use package 20 | "github.com/labulakalia/wazero_net/wasi/http" 21 | "github.com/labulakalia/wazero_net/wasi/net" 22 | */ 23 | 24 | type PluginImpl struct { 25 | localpath *plugin.Formdata_FormItem_DirPathValue 26 | uPath string 27 | } 28 | 29 | func NewPluginImpl() *PluginImpl { 30 | return &PluginImpl{ 31 | localpath: plugin.DirPath(""), 32 | } 33 | } 34 | 35 | // Id implements IPlugin. 36 | func (p *PluginImpl) PluginId() (string, error) { 37 | return "local", nil 38 | } 39 | 40 | // GetAuthType implements IPlugin. 41 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 42 | 43 | formData := &plugin.AuthMethod_Formdata{ 44 | Formdata: &plugin.Formdata{ 45 | FormItems: []*plugin.Formdata_FormItem{ 46 | { 47 | Name: "Directory", 48 | Value: p.localpath, 49 | }, 50 | }, 51 | }, 52 | } 53 | 54 | return &plugin.Auth{ 55 | AuthMethods: []*plugin.AuthMethod{&plugin.AuthMethod{Method: formData}}, 56 | }, nil 57 | } 58 | 59 | // CheckAuth implements IPlugin. 60 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 61 | authDataBytes, err := authMethod.MarshalVT() 62 | if err != nil { 63 | return nil, err 64 | } 65 | authData = &plugin.AuthData{ 66 | AuthDataBytes: authDataBytes, 67 | } 68 | return authData, nil 69 | } 70 | 71 | // InitAuth implements IPlugin. 72 | func (p *PluginImpl) CheckAuthData(authData []byte) error { 73 | authMethod := &plugin.AuthMethod{} 74 | err := authMethod.UnmarshalVT(authData) 75 | if err != nil { 76 | return err 77 | } 78 | fmt.Println("check auth data", authMethod.Method.(*plugin.AuthMethod_Formdata)) 79 | dirPath := authMethod.Method.(*plugin.AuthMethod_Formdata).Formdata.FormItems[0].Value.(*plugin.Formdata_FormItem_DirPathValue) 80 | p.localpath.DirPathValue = dirPath.DirPathValue 81 | p.uPath = dirPath.DirPathValue.Value 82 | if strings.Contains(p.uPath, ":") { 83 | p.uPath = `/` + strings.ReplaceAll(strings.ReplaceAll(dirPath.DirPathValue.Value, ":", ""), `\`, "/") 84 | } 85 | _, err = os.Stat(p.uPath) 86 | if err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | // AuthId implements IPlugin. 93 | func (p *PluginImpl) PluginAuthId() (string, error) { 94 | return fmt.Sprintf("%x", md5.Sum(util.StringToBytes(&p.localpath.DirPathValue.Value))), nil 95 | } 96 | 97 | // GetDirEntry implements IPlugin. 98 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 99 | dirPath := req.Path 100 | page := req.Page 101 | pageSize := req.PageSize 102 | 103 | entries, err := os.ReadDir(filepath.Join(p.uPath, dirPath)) 104 | if err != nil { 105 | return nil, err 106 | } 107 | dirEntry := &plugin.DirEntry{ 108 | FileEntries: []*plugin.FileEntry{}, 109 | } 110 | start := int((page - 1) * pageSize) 111 | end := start + int(pageSize) 112 | 113 | if len(entries) <= start { 114 | return dirEntry, nil 115 | } else if len(entries) >= end { 116 | entries = entries[start:end] 117 | } else { 118 | entries = entries[start:] 119 | } 120 | for _, entry := range entries { 121 | fileEntry := &plugin.FileEntry{ 122 | Name: entry.Name(), 123 | } 124 | if entry.IsDir() { 125 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 126 | } else { 127 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 128 | } 129 | fileInfo, err := entry.Info() 130 | if err != nil { 131 | slog.Error("Failed to get file info", "error", err) 132 | continue 133 | } 134 | fileEntry.Size = uint64(fileInfo.Size()) 135 | stat, ok := fileInfo.Sys().(*syscall.Stat_t) 136 | if ok { 137 | fileEntry.AccessedTime = uint64(stat.Atim.Sec) 138 | fileEntry.ModifiedTime = uint64(stat.Mtim.Sec) 139 | fileEntry.CreatedTime = uint64(stat.Ctim.Sec) 140 | } 141 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 142 | } 143 | return dirEntry, nil 144 | } 145 | 146 | // GetFileResource implements IPlugin. 147 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 148 | statPath := filepath.Join(p.uPath, req.FilePath) 149 | if strings.Contains(statPath, `\`) { 150 | statPath = strings.ReplaceAll(statPath, `\`, `/`) 151 | } 152 | _, err := os.Stat(statPath) 153 | if err != nil { 154 | return nil, err 155 | } 156 | return &plugin.FileResource{ 157 | FileResourceData: []*plugin.FileResource_FileResourceData{ 158 | { 159 | Url: fmt.Sprintf("file://%s", filepath.Join(p.localpath.DirPathValue.Value, req.FilePath)), 160 | ResourceType: plugin.FileResource_Video, 161 | Resolution: plugin.FileResource_Original, 162 | }, 163 | }, 164 | }, nil 165 | } 166 | -------------------------------------------------------------------------------- /quark/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /quark/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /quark/model.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/medianexapp/plugin_api/plugin" 4 | 5 | type Response struct { 6 | Status int 7 | Code int 8 | Message string 9 | Data any 10 | } 11 | 12 | type FileData struct { 13 | List []File `json:"list"` 14 | } 15 | 16 | type File struct { 17 | Fid string `json:"fid"` 18 | FileName string `json:"file_name"` 19 | Size uint64 `json:"size"` 20 | FileType uint64 `json:"file_type"` 21 | CreatedAt uint64 `json:"created_at"` 22 | UpdatedAt uint64 `json:"updated_at"` 23 | Dir bool `json:"dir"` 24 | File bool `json:"file"` 25 | UpdatedViewAt uint64 `json:"updated_view_at"` 26 | 27 | DownloadUrl string `json:"download_url"` 28 | } 29 | 30 | type PlayReq struct { 31 | Fid string `json:"fid"` 32 | Resolutions string `json:"resolutions"` 33 | Supports string `json:"supports"` 34 | } 35 | 36 | type PlayData struct { 37 | VideoList []VideoList `json:"video_list"` 38 | } 39 | 40 | type VideoList struct { 41 | Resolution string `json:"resolution"` 42 | TransStatus string `json:"trans_status"` // success 43 | VideoInfo struct { 44 | URL string `json:"url"` 45 | } `json:"video_info"` 46 | } 47 | 48 | var resolutionMap = map[string]plugin.FileResource_Resolution{ 49 | "4k": plugin.FileResource_UHD, 50 | "super": plugin.FileResource_FHD, 51 | "high": plugin.FileResource_HD, 52 | "low": plugin.FileResource_LD, 53 | } 54 | -------------------------------------------------------------------------------- /quark/plugin.go: -------------------------------------------------------------------------------- 1 | // Code generated by plugin_api. DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /quark/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "quark" 2 | Name = "Quark" 3 | desc = "quark plugin desc" 4 | icon = "quark.png" 5 | author = ["[]"] 6 | version = "v0.0.3" 7 | changelog = ["fix player original failed"] 8 | -------------------------------------------------------------------------------- /quark/plugin_impl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net/http" 11 | "net/url" 12 | "strconv" 13 | "strings" 14 | "time" 15 | 16 | _ "github.com/labulakalia/wazero_net/wasi/http" // if you need http import this 17 | "github.com/medianexapp/plugin_api/httpclient" 18 | "github.com/medianexapp/plugin_api/plugin" 19 | "github.com/medianexapp/plugin_api/ratelimit" 20 | ) 21 | 22 | const ( 23 | userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) quark-cloud-drive/3.20.0 Chrome/112.0.5615.165 Electron/24.1.3.8 Safari/537.36 Channel/pckk_other_ch" 24 | referer = "https://pan.quark.cn" 25 | api = "https://drive-pc.quark.cn/1/clouddrive" 26 | pr = "ucpro" 27 | ) 28 | 29 | type PluginImpl struct { 30 | cookie string 31 | client *httpclient.Client 32 | ratelimit *ratelimit.RateLimit 33 | } 34 | 35 | func NewPluginImpl() *PluginImpl { 36 | slog.SetLogLoggerLevel(slog.LevelDebug) 37 | limitConfigMap := map[string]ratelimit.LimitConfig{ 38 | "": ratelimit.LimitConfig{ 39 | Limit: 1, 40 | Duration: time.Second, 41 | }, 42 | } 43 | return &PluginImpl{ 44 | client: httpclient.NewClient(httpclient.WithUserAgent(userAgent)), 45 | ratelimit: ratelimit.New(limitConfigMap), 46 | } 47 | } 48 | 49 | // Id implements IPlugin. 50 | func (p *PluginImpl) PluginId() (string, error) { 51 | return "quark", nil 52 | } 53 | 54 | // GetAuth return how to auth 55 | // 1.FormData input data 56 | // 2.Callback use url callback auth,like oauth 57 | // 3.Scanqrcode,return qrcode image to auth 58 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 59 | slog.Info("GetAuth") 60 | auth := &plugin.Auth{ 61 | AuthMethods: []*plugin.AuthMethod{ 62 | { 63 | Method: &plugin.AuthMethod_Formdata{ 64 | Formdata: &plugin.Formdata{ 65 | FormItems: []*plugin.Formdata_FormItem{ 66 | { 67 | Name: "Cookie", 68 | Value: plugin.String(""), 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }, 75 | } 76 | return auth, nil 77 | } 78 | 79 | // CheckAuthMethod check auth is finished and return authDataBytes and authData's expired time 80 | // if authmethod's type is *plugin.AuthMethod_Refresh,you need to refresh token 81 | // assert authMethod.Method's type to check auth is finished,return auth data and expired time if authed 82 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (*plugin.AuthData, error) { 83 | slog.Debug("CheckAuthMethod", "authMethod", authMethod) 84 | authDataBytes, err := authMethod.Method.(*plugin.AuthMethod_Formdata).Formdata.MarshalVT() 85 | if err != nil { 86 | return nil, err 87 | } 88 | return &plugin.AuthData{AuthDataBytes: authDataBytes}, nil 89 | } 90 | 91 | // CheckAuthData use authDataBytes to uath 92 | // you must store auth data to *PluginImpl 93 | func (p *PluginImpl) CheckAuthData(authDataBytes []byte) error { 94 | slog.Debug("CheckAuthData", "authDataBytes", authDataBytes) 95 | formdata := &plugin.Formdata{} 96 | if err := formdata.UnmarshalVT(authDataBytes); err != nil { 97 | return err 98 | } 99 | p.cookie = formdata.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value 100 | err := p.request("/config", http.MethodGet, nil, nil, nil) 101 | if err != nil { 102 | return err 103 | } 104 | // fmt.Println("check auth data", string(res)) 105 | // https://pan.quark.cn/account/info?fr=pc&platform=pc 106 | return nil 107 | } 108 | 109 | // PluginAuthId implements IPlugin. 110 | // plugin auth id,you can generate id by md5 or sha 111 | func (p *PluginImpl) PluginAuthId() (string, error) { 112 | return "quark", nil 113 | } 114 | 115 | // GetDirEntry implements IPlugin. 116 | // return dir file entry 117 | // save your driver file raw data to FileEntry.RawData,you can get it after GetDirEntry and GetFileResource request 118 | // default page_size if 100,if this not for you,change is on DirEntry.PageSize,will use new PageSize for next request 119 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 120 | slog.Debug("GetDirEntry", "req", req) 121 | var pdirFid string 122 | if req.Path == "/" { 123 | pdirFid = "0" 124 | } else { 125 | file := File{} 126 | if req.FileEntry == nil || req.FileEntry.RawData == nil { 127 | return nil, errors.New("file entry is nil") 128 | } 129 | err := json.Unmarshal(req.FileEntry.RawData, &file) 130 | if err != nil { 131 | return nil, err 132 | } 133 | pdirFid = file.Fid 134 | } 135 | if req.PageSize > 50 { 136 | req.PageSize = 50 137 | } 138 | u := url.Values{} 139 | u.Add("pdir_fid", pdirFid) 140 | u.Add("_page", fmt.Sprint(req.Page)) 141 | u.Add("_size", fmt.Sprint(req.PageSize)) 142 | u.Add("_fetch_total", "1") 143 | fileData := &FileData{ 144 | List: []File{}, 145 | } 146 | err := p.request("/file/sort", http.MethodGet, u, nil, fileData) 147 | if err != nil { 148 | return nil, err 149 | } 150 | dirEntry := &plugin.DirEntry{ 151 | PageSize: 50, 152 | FileEntries: []*plugin.FileEntry{}, 153 | } 154 | for _, file := range fileData.List { 155 | fileEntry := &plugin.FileEntry{ 156 | Name: file.FileName, 157 | Size: file.Size, 158 | CreatedTime: file.CreatedAt / 1000, 159 | ModifiedTime: file.UpdatedAt / 1000, 160 | AccessedTime: file.UpdatedViewAt, 161 | } 162 | if file.File { 163 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 164 | } else { 165 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 166 | } 167 | fileRawData, err := json.Marshal(file) 168 | if err == nil { 169 | fileEntry.RawData = fileRawData 170 | } 171 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 172 | } 173 | return dirEntry, nil 174 | } 175 | 176 | // GetFileResource implements IPlugin. 177 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 178 | slog.Debug("GetFileResource", "req", req) 179 | file := File{} 180 | if req.FileEntry == nil || req.FileEntry.RawData == nil { 181 | return nil, errors.New("file entry is nil") 182 | } 183 | err := json.Unmarshal(req.FileEntry.RawData, &file) 184 | if err != nil { 185 | return nil, err 186 | } 187 | fileResource := &plugin.FileResource{ 188 | FileResourceData: []*plugin.FileResource_FileResourceData{}, 189 | } 190 | data := map[string][]string{ 191 | "fids": []string{file.Fid}, 192 | } 193 | respData := []File{} 194 | err = p.request("/file/download", http.MethodPost, nil, data, &respData) 195 | if err != nil { 196 | return nil, err 197 | } 198 | if len(respData) == 1 { 199 | expireTime, err := getExpires(respData[0].DownloadUrl) 200 | if err != nil { 201 | slog.Error("get expires failed", "url", respData[0].DownloadUrl, "err", err) 202 | } else { 203 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 204 | Url: respData[0].DownloadUrl, 205 | Resolution: plugin.FileResource_Original, 206 | ResourceType: plugin.FileResource_Video, 207 | Header: map[string]string{ 208 | "Cookie": p.cookie, 209 | "Referer": referer, 210 | "User-Agent": userAgent, 211 | }, 212 | ExpireTime: expireTime, 213 | Size: req.FileEntry.Size, 214 | Proxy: true, 215 | ProxyChunkParallel: 3, 216 | ProxyChunkSize: 1024 * 1024 * 5, 217 | }) 218 | } 219 | } 220 | // 获取播放链接 221 | uri := "/file/v2/play" 222 | reqData := PlayReq{ 223 | Fid: file.Fid, 224 | Resolutions: "normal,low,high,super,2k,4k", 225 | Supports: "fmp4,m3u8", 226 | } 227 | u := url.Values{} 228 | u.Add("uc_param_str", "") 229 | playData := PlayData{ 230 | VideoList: []VideoList{}, 231 | } 232 | err = p.request(uri, http.MethodPost, u, reqData, &playData) 233 | if err != nil { 234 | return nil, err 235 | } 236 | // 4k 237 | // super 2k 238 | // 1080p 239 | // 720p 240 | 241 | for _, item := range playData.VideoList { 242 | if item.VideoInfo.URL == "" { 243 | continue 244 | } 245 | expireTime, err := getExpires(item.VideoInfo.URL) 246 | if err != nil { 247 | slog.Error("get expires failed", "url", item.VideoInfo.URL, "err", err) 248 | 249 | } 250 | fileResource.FileResourceData = append(fileResource.FileResourceData, &plugin.FileResource_FileResourceData{ 251 | Url: item.VideoInfo.URL, 252 | Resolution: resolutionMap[item.Resolution], 253 | ResourceType: plugin.FileResource_Video, 254 | Header: map[string]string{ 255 | "Cookie": p.cookie, 256 | "Referer": referer, 257 | "User-Agent": userAgent, 258 | }, 259 | ExpireTime: expireTime, 260 | }) 261 | } 262 | return fileResource, nil 263 | } 264 | 265 | func (p *PluginImpl) request(uri string, method string, u url.Values, reqData, respData any) error { 266 | if u == nil { 267 | u = url.Values{} 268 | } 269 | p.ratelimit.Wait("") 270 | u.Add("pr", pr) 271 | u.Add("fr", "pc") 272 | var body io.Reader 273 | if reqData != nil { 274 | data, err := json.Marshal(reqData) 275 | if err != nil { 276 | return err 277 | } 278 | body = bytes.NewBuffer(data) 279 | } 280 | req, err := http.NewRequest(method, fmt.Sprintf("%s%s?%s", api, uri, u.Encode()), body) 281 | if err != nil { 282 | return err 283 | } 284 | req.Header.Set("Cookie", p.cookie) 285 | req.Header.Set("Accept", "application/json, text/plain, */*") 286 | req.Header.Set("Referer", referer) 287 | req.Header.Set("User-Agent", userAgent) 288 | resp, err := http.DefaultClient.Do(req) 289 | if err != nil { 290 | return err 291 | } 292 | for _, cookie := range resp.Cookies() { 293 | if cookie.Name == "__puus" { 294 | h := http.Header{} 295 | h.Add("Cookie", p.cookie) 296 | cookieStrs := []string{} 297 | r := http.Request{Header: h} 298 | for _, oldCookie := range r.Cookies() { 299 | oldCookieStr := oldCookie.String() 300 | if oldCookie.Name == "__puus" { 301 | oldCookieStr = cookie.String() 302 | } 303 | cookieStrs = append(cookieStrs, oldCookieStr) 304 | } 305 | p.cookie = strings.Join(cookieStrs, ";") 306 | } 307 | } 308 | respBytes, err := io.ReadAll(resp.Body) 309 | if err != nil { 310 | return err 311 | } 312 | response := Response{ 313 | Data: respData, 314 | } 315 | err = json.Unmarshal(respBytes, &response) 316 | if err != nil { 317 | return err 318 | } 319 | if response.Code != 0 { 320 | slog.Error("resp code failed", "response", response) 321 | return fmt.Errorf("%s", response.Message) 322 | } 323 | 324 | defer resp.Body.Close() 325 | return nil 326 | } 327 | 328 | func getExpires(u string) (uint64, error) { 329 | p, err := url.Parse(u) 330 | if err != nil { 331 | return 0, err 332 | } 333 | expires := p.Query().Get("Expires") 334 | epInt, err := strconv.Atoi(expires) 335 | if err != nil { 336 | p, _ = url.Parse(u) 337 | sp := strings.Split(p.Query().Get("auth_key"), "-") 338 | if len(sp) > 0 { 339 | epInt, err = strconv.Atoi(sp[0]) 340 | if err != nil { 341 | return 0, err 342 | } 343 | } 344 | } 345 | return uint64(epInt), nil 346 | } 347 | -------------------------------------------------------------------------------- /quark/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/medianexapp/plugin_api/plugin" 8 | ) 9 | 10 | func TestPluginImpl(t *testing.T) { 11 | cookies := "" 12 | p := NewPluginImpl() 13 | auth, _ := p.GetAuth() 14 | method := auth.AuthMethods[0].Method 15 | 16 | method.(*plugin.AuthMethod_Formdata).Formdata.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value = cookies 17 | authData, err := p.CheckAuthMethod(&plugin.AuthMethod{ 18 | Method: auth.AuthMethods[0].Method, 19 | }) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | err = p.CheckAuthData(authData.AuthDataBytes) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | resp, err := p.GetDirEntry(&plugin.GetDirEntryRequest{ 28 | Path: "/", 29 | Page: 1, 30 | PageSize: 30, 31 | }) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | t.Logf("GetFileResource %s\n", resp.FileEntries[0].RawData) 36 | resources, err := p.GetFileResource(&plugin.GetFileResourceRequest{ 37 | FilePath: "/Unicorn.Academy.S01E02.The.Hidden.Temple.1080p.NF.WEB-DL.DDP5.1.H.264-FLUX.mkv", 38 | FileEntry: resp.FileEntries[0], 39 | }) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | f := resources.FileResourceData[0] 44 | t.Log(f.Url) 45 | 46 | dd, _ := json.Marshal(f.Header) 47 | t.Log(string(dd)) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /quark/quark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/quark/quark.png -------------------------------------------------------------------------------- /sftp/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /sftp/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /sftp/plugin.go: -------------------------------------------------------------------------------- 1 | // DO NOT EDIT. 2 | //go:build wasip1 3 | 4 | package main 5 | 6 | import ( 7 | "github.com/medianexapp/plugin_api" 8 | ) 9 | 10 | func init() { 11 | plugin_api.RegistryPlugin(NewPluginImpl()) 12 | } 13 | 14 | func main() {} 15 | -------------------------------------------------------------------------------- /sftp/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "sftp" 2 | name = "Sftp" 3 | desc = "sftp driver plugin" 4 | icon = "sftp.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.1" 7 | changelog = ["sftp driver plugin init"] 8 | -------------------------------------------------------------------------------- /sftp/plugin_impl.go: -------------------------------------------------------------------------------- 1 | //go:build wasip1 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/md5" 7 | "fmt" 8 | "log/slog" 9 | "net/netip" 10 | "os" 11 | "strings" 12 | "time" 13 | 14 | "github.com/medianexapp/plugin_api/plugin" 15 | "github.com/medianexapp/sftp" 16 | "golang.org/x/crypto/ssh" 17 | 18 | "github.com/labulakalia/wazero_net/util" 19 | wasi_net "github.com/labulakalia/wazero_net/wasi/net" 20 | ) 21 | 22 | /* 23 | NOTE: net and http use package 24 | "github.com/labulakalia/wazero_net/wasi/http" 25 | "github.com/labulakalia/wazero_net/wasi/net" 26 | */ 27 | 28 | type PluginImpl struct { 29 | sftpClient *sftp.Client 30 | 31 | sftpAuth *sftpAuth 32 | } 33 | 34 | func NewPluginImpl() *PluginImpl { 35 | slog.SetLogLoggerLevel(slog.LevelDebug) 36 | return &PluginImpl{ 37 | sftpAuth: &sftpAuth{ 38 | Addr: plugin.String("127.0.0.1"), 39 | User: plugin.String(""), 40 | Password: plugin.ObscureString(""), 41 | }, 42 | } 43 | } 44 | 45 | type sftpAuth struct { 46 | Addr *plugin.Formdata_FormItem_StringValue 47 | User *plugin.Formdata_FormItem_StringValue 48 | Password *plugin.Formdata_FormItem_ObscureStringValue 49 | } 50 | 51 | // Id implements IPlugin. 52 | func (p *PluginImpl) PluginId() (string, error) { 53 | return "sftp", nil 54 | } 55 | 56 | // GetAuthType implements IPlugin. 57 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 58 | formData := &plugin.AuthMethod_Formdata{ 59 | Formdata: &plugin.Formdata{ 60 | FormItems: []*plugin.Formdata_FormItem{ 61 | { 62 | Name: "Addr", 63 | Value: p.sftpAuth.Addr, 64 | }, 65 | { 66 | Name: "User", 67 | Value: p.sftpAuth.User, 68 | }, 69 | { 70 | Name: "Password", 71 | Value: p.sftpAuth.Password, 72 | }, 73 | }, 74 | }, 75 | } 76 | return &plugin.Auth{ 77 | AuthMethods: []*plugin.AuthMethod{ 78 | &plugin.AuthMethod{ 79 | Method: formData, 80 | }, 81 | }, 82 | }, nil 83 | } 84 | 85 | // CheckAuth implements IPlugin. 86 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 87 | 88 | authDataBytes, err := authMethod.MarshalVT() 89 | if err != nil { 90 | return nil, err 91 | } 92 | authData = &plugin.AuthData{ 93 | AuthDataBytes: authDataBytes, 94 | } 95 | return authData, nil 96 | } 97 | 98 | func (p *PluginImpl) connectSftp() error { 99 | if p.sftpClient != nil { 100 | p.sftpClient.Close() 101 | } 102 | addr := p.sftpAuth.Addr.StringValue.Value 103 | _, err := netip.ParseAddrPort(addr) 104 | if err != nil { 105 | addr = fmt.Sprintf("%s:%d", strings.TrimRight(addr, ":"), 22) 106 | } 107 | 108 | conn, err := wasi_net.Dial("tcp", addr) 109 | if err != nil { 110 | slog.Error("dial failed", "err", err) 111 | return err 112 | } 113 | config := &ssh.ClientConfig{ 114 | User: p.sftpAuth.User.StringValue.Value, 115 | Auth: []ssh.AuthMethod{ 116 | ssh.Password(p.sftpAuth.Password.ObscureStringValue.Value), 117 | // ssh.PublicKeys(signer), 118 | }, 119 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 120 | Timeout: time.Second, 121 | } 122 | c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) 123 | if err != nil { 124 | slog.Error("client conn failed", "err", err) 125 | return err 126 | } 127 | sftpClient, err := sftp.NewClient(ssh.NewClient(c, chans, reqs)) 128 | if err != nil { 129 | slog.Error("sftp client failed", "err", err) 130 | return err 131 | } 132 | p.sftpClient = sftpClient 133 | return nil 134 | } 135 | 136 | func (p *PluginImpl) unmarshalFormData(formData *plugin.Formdata) { 137 | p.sftpAuth.Addr.StringValue = formData.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue 138 | p.sftpAuth.User.StringValue = formData.FormItems[1].Value.(*plugin.Formdata_FormItem_StringValue).StringValue 139 | p.sftpAuth.Password.ObscureStringValue = formData.FormItems[2].Value.(*plugin.Formdata_FormItem_ObscureStringValue).ObscureStringValue 140 | 141 | } 142 | 143 | // InitAuth implements IPlugin. 144 | func (p *PluginImpl) CheckAuthData(AuthDataBytes []byte) error { 145 | slog.Debug("start check auth data") 146 | authMethod := &plugin.AuthMethod{} 147 | err := authMethod.UnmarshalVT(AuthDataBytes) 148 | if err != nil { 149 | return err 150 | } 151 | formData := authMethod.Method.(*plugin.AuthMethod_Formdata) 152 | p.unmarshalFormData(formData.Formdata) 153 | err = p.connectSftp() 154 | if err != nil { 155 | return err 156 | } 157 | slog.Debug("check auth data success") 158 | return nil 159 | } 160 | 161 | // AuthId implements IPlugin. 162 | func (p *PluginImpl) PluginAuthId() (string, error) { 163 | id := fmt.Sprintf("%s%s%s", p.sftpAuth.Addr.StringValue.Value, p.sftpAuth.User.StringValue.Value, p.sftpAuth.Password.ObscureStringValue.Value) 164 | return fmt.Sprintf("%x", md5.Sum(util.StringToBytes(&id))), nil 165 | } 166 | 167 | // GetDirEntry implements IPlugin. 168 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 169 | dirPath := req.Path 170 | page := req.Page 171 | pageSize := req.PageSize 172 | var ( 173 | entries []os.FileInfo 174 | err error 175 | ) 176 | for range 3 { 177 | entries, err = p.sftpClient.ReadDir(dirPath) 178 | if err != nil { 179 | slog.Error("sftp client read dir failed", "err", err) 180 | p.connectSftp() 181 | } else { 182 | break 183 | } 184 | } 185 | 186 | dirEntry := &plugin.DirEntry{ 187 | FileEntries: make([]*plugin.FileEntry, 0, len(entries)), 188 | } 189 | 190 | start := int((page - 1) * pageSize) 191 | end := start + int(pageSize) 192 | 193 | if len(entries) <= start { 194 | return dirEntry, nil 195 | } else if len(entries) >= end { 196 | entries = entries[start:end] 197 | } else { 198 | entries = entries[start:] 199 | } 200 | 201 | for _, entry := range entries { 202 | fileEntry := &plugin.FileEntry{ 203 | Size: uint64(entry.Size()), 204 | Name: entry.Name(), 205 | } 206 | if entry.IsDir() { 207 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 208 | } else { 209 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 210 | } 211 | stat, ok := entry.Sys().(*sftp.FileStat) 212 | 213 | if ok { 214 | fileEntry.CreatedTime = uint64(stat.Mtime) 215 | fileEntry.ModifiedTime = uint64(stat.Mode) 216 | fileEntry.AccessedTime = uint64(stat.Atime) 217 | } 218 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 219 | } 220 | return dirEntry, nil 221 | } 222 | 223 | // GetFileResource implements IPlugin. 224 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 225 | // sftp://[user[:password]@]server[:port]/path/to/remote/resource.mpeg 226 | var err error 227 | for range 3 { 228 | _, err = p.sftpClient.Stat(req.FilePath) 229 | if err != nil { 230 | slog.Error("sftp stat failed", "err", err) 231 | p.connectSftp() 232 | } else { 233 | break 234 | } 235 | } 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | userPass := "" 241 | if p.sftpAuth.User.StringValue.Value != "" && p.sftpAuth.Password.ObscureStringValue.Value != "" { 242 | userPass = fmt.Sprintf("%s:%s@", p.sftpAuth.User.StringValue.Value, p.sftpAuth.Password.ObscureStringValue.Value) 243 | } 244 | fileUrl := fmt.Sprintf("sftp://%s%s%s", userPass, p.sftpAuth.Addr.StringValue.Value, req.FilePath) 245 | return &plugin.FileResource{ 246 | FileResourceData: []*plugin.FileResource_FileResourceData{ 247 | { 248 | Url: fileUrl, 249 | Resolution: plugin.FileResource_Original, 250 | ResourceType: plugin.FileResource_Video, 251 | }, 252 | }, 253 | }, nil 254 | } 255 | -------------------------------------------------------------------------------- /sftp/sftp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/sftp/sftp.png -------------------------------------------------------------------------------- /smb/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /smb/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 labulakalia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /smb/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /smb/plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/medianexapp/plugin_api" 5 | ) 6 | 7 | func init() { 8 | plugin_api.RegistryPlugin(NewPluginImpl()) 9 | } 10 | 11 | func main() {} 12 | -------------------------------------------------------------------------------- /smb/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "smb" 2 | name = "Smb" 3 | desc = "smb driver plugin" 4 | icon = "smb.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.1" 7 | changelog = ["smb driver plugin init"] 8 | -------------------------------------------------------------------------------- /smb/plugin_impl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/md5" 6 | "fmt" 7 | "log/slog" 8 | "net/netip" 9 | "os" 10 | "strings" 11 | 12 | "github.com/labulakalia/wazero_net/util" 13 | wasi_net "github.com/labulakalia/wazero_net/wasi/net" 14 | "github.com/medianexapp/go-smb2" 15 | "github.com/medianexapp/plugin_api/plugin" 16 | ) 17 | 18 | /* 19 | NOTE: net and http use package 20 | "github.com/labulakalia/wazero_net/wasi/http" 21 | "github.com/labulakalia/wazero_net/wasi/net" 22 | */ 23 | 24 | type PluginImpl struct { 25 | session *smb2.Session 26 | shares map[string]*smb2.Share 27 | 28 | sambaAuth *sambaAuth 29 | } 30 | 31 | func NewPluginImpl() *PluginImpl { 32 | return &PluginImpl{ 33 | sambaAuth: &sambaAuth{ 34 | Addr: plugin.String("127.0.0.1"), 35 | User: plugin.String(""), 36 | Password: plugin.ObscureString(""), 37 | }, 38 | } 39 | 40 | } 41 | 42 | type sambaAuth struct { 43 | Addr *plugin.Formdata_FormItem_StringValue 44 | User *plugin.Formdata_FormItem_StringValue 45 | Password *plugin.Formdata_FormItem_ObscureStringValue 46 | } 47 | 48 | // Id implements IPlugin. 49 | func (p *PluginImpl) PluginId() (string, error) { 50 | return "smb", nil 51 | } 52 | 53 | // GetAuth implements IPlugin. 54 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 55 | formData := &plugin.AuthMethod_Formdata{ 56 | Formdata: &plugin.Formdata{ 57 | FormItems: []*plugin.Formdata_FormItem{ 58 | 59 | { 60 | Name: "Addr", 61 | Value: p.sambaAuth.Addr, 62 | }, 63 | { 64 | Name: "User", 65 | Value: p.sambaAuth.User, 66 | }, 67 | { 68 | Name: "Password", 69 | Value: p.sambaAuth.Password, 70 | }, 71 | }, 72 | }, 73 | } 74 | return &plugin.Auth{ 75 | AuthMethods: []*plugin.AuthMethod{ 76 | &plugin.AuthMethod{ 77 | Method: formData, 78 | }, 79 | }, 80 | }, nil 81 | } 82 | 83 | func (p *PluginImpl) unmarshalFormData(formData *plugin.Formdata) { 84 | p.sambaAuth.Addr.StringValue = formData.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue 85 | p.sambaAuth.User.StringValue = formData.FormItems[1].Value.(*plugin.Formdata_FormItem_StringValue).StringValue 86 | p.sambaAuth.Password.ObscureStringValue = formData.FormItems[2].Value.(*plugin.Formdata_FormItem_ObscureStringValue).ObscureStringValue 87 | 88 | } 89 | 90 | // CheckAuth implements IPlugin. 91 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 92 | authDataBytes, err := authMethod.MarshalVT() 93 | if err != nil { 94 | return nil, err 95 | } 96 | authData = &plugin.AuthData{ 97 | AuthDataBytes: authDataBytes, 98 | } 99 | return authData, nil 100 | } 101 | 102 | // InitAuth implements IPlugin. 103 | func (p *PluginImpl) CheckAuthData(authDataBytes []byte) error { 104 | authMethod := &plugin.AuthMethod{} 105 | err := authMethod.UnmarshalVT(authDataBytes) 106 | if err != nil { 107 | return err 108 | } 109 | p.unmarshalFormData(authMethod.Method.(*plugin.AuthMethod_Formdata).Formdata) 110 | 111 | addr := p.sambaAuth.Addr.StringValue.Value 112 | _, err = netip.ParseAddrPort(addr) 113 | if err != nil { 114 | addr = fmt.Sprintf("%s:%d", strings.TrimRight(addr, ":"), 445) 115 | } 116 | slog.Info("start dial tcp", "addr", addr) 117 | conn, err := wasi_net.Dial("tcp", addr) 118 | if err != nil { 119 | slog.Error("dial failed", "err", err) 120 | return err 121 | } 122 | 123 | user := p.sambaAuth.User.StringValue.Value 124 | passwd := p.sambaAuth.Password.ObscureStringValue.Value 125 | if user == "" && passwd == "" { 126 | user = "guest" 127 | passwd = "guest" 128 | } 129 | smbDialer := &smb2.Dialer{ 130 | Initiator: &smb2.NTLMInitiator{ 131 | User: user, 132 | Password: passwd, 133 | }, 134 | } 135 | 136 | smbSession, err := smbDialer.DialConn(context.Background(), conn, addr) 137 | if err != nil { 138 | slog.Error("failed to dial", "error", err) 139 | return err 140 | } 141 | 142 | p.session = smbSession 143 | shareNames, err := smbSession.ListSharenames() 144 | if err != nil { 145 | return err 146 | } 147 | 148 | p.shares = make(map[string]*smb2.Share) 149 | for _, shareName := range shareNames { 150 | if strings.HasSuffix(shareName, "$") { 151 | continue 152 | } 153 | share, err := smbSession.Mount(shareName) 154 | if err != nil { 155 | slog.Error("mount failed", "sharename", shareName, "error", err) 156 | continue 157 | } 158 | p.shares[shareName] = share 159 | slog.Info("smb session mount", "share name", shareName) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // AuthId implements IPlugin. 166 | func (p *PluginImpl) PluginAuthId() (string, error) { 167 | id := fmt.Sprintf("%s%s%s", p.sambaAuth.Addr.StringValue.Value, p.sambaAuth.User.StringValue.Value, p.sambaAuth.Password.ObscureStringValue.Value) 168 | return fmt.Sprintf("%x", md5.Sum(util.StringToBytes(&id))), nil 169 | } 170 | 171 | func (p *PluginImpl) checkShare(dir_path string) (*smb2.Share, string, error) { 172 | sp := strings.Split(dir_path, "/")[1:] 173 | shareName := sp[0] 174 | share, ok := p.shares[shareName] 175 | if !ok { 176 | return nil, "", fmt.Errorf("%s not exist", dir_path) 177 | } 178 | return share, strings.Join(sp[1:], "/"), nil 179 | } 180 | 181 | // GetDirEntry implements IPlugin. 182 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 183 | dirPath := req.Path 184 | page := req.Page 185 | pageSize := req.PageSize 186 | dirEntry := &plugin.DirEntry{ 187 | FileEntries: []*plugin.FileEntry{}, 188 | } 189 | if dirPath == "/" { 190 | for name := range p.shares { 191 | dirEntry.FileEntries = append(dirEntry.FileEntries, &plugin.FileEntry{ 192 | Name: name, 193 | FileType: plugin.FileEntry_FileTypeDir, 194 | }) 195 | } 196 | return dirEntry, nil 197 | } 198 | 199 | share, smbPath, err := p.checkShare(dirPath) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | fileInfos, err := share.ReadDir(smbPath) 205 | if err != nil { 206 | return nil, err 207 | } 208 | newFileInfos := []os.FileInfo{} 209 | for _, fileinfo := range fileInfos { 210 | if strings.HasPrefix(fileinfo.Name(), ".") { 211 | continue 212 | } 213 | newFileInfos = append(newFileInfos, fileinfo) 214 | } 215 | fileInfos = newFileInfos 216 | start := int((page - 1) * pageSize) 217 | end := start + int(pageSize) 218 | 219 | if len(fileInfos) <= start { 220 | return dirEntry, nil 221 | } else if len(fileInfos) >= end { 222 | fileInfos = fileInfos[start:end] 223 | } else { 224 | fileInfos = fileInfos[start:] 225 | } 226 | 227 | for _, fileinfo := range fileInfos { 228 | fileEntry := &plugin.FileEntry{ 229 | Name: fileinfo.Name(), 230 | Size: uint64(fileinfo.Size()), 231 | } 232 | if fileinfo.IsDir() { 233 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 234 | } else { 235 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 236 | } 237 | fileStat, ok := fileinfo.Sys().(*smb2.FileStat) 238 | if ok { 239 | fileEntry.CreatedTime = uint64(fileStat.ChangeTime.Unix()) 240 | fileEntry.ModifiedTime = uint64(fileStat.LastWriteTime.Unix()) 241 | fileEntry.AccessedTime = uint64(fileStat.LastAccessTime.Unix()) 242 | } 243 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 244 | } 245 | return dirEntry, nil 246 | } 247 | 248 | // GetFileResource implements IPlugin. 249 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 250 | // smb://[[domain:]user[:password@]]server[/share[/path[/file]]] 251 | share, smbPath, err := p.checkShare(req.FilePath) 252 | if err != nil { 253 | return nil, err 254 | } 255 | _, err = share.Stat(smbPath) 256 | if err != nil { 257 | return nil, err 258 | } 259 | userPass := p.sambaAuth.User.StringValue.Value 260 | if p.sambaAuth.Password.ObscureStringValue.Value != "" { 261 | userPass = fmt.Sprintf("%s:%s@", p.sambaAuth.User.StringValue.Value, p.sambaAuth.Password.ObscureStringValue.Value) 262 | } 263 | fileUrl := fmt.Sprintf("smb://%s%s%s", userPass, p.sambaAuth.Addr.StringValue.Value, req.FilePath) 264 | return &plugin.FileResource{ 265 | FileResourceData: []*plugin.FileResource_FileResourceData{ 266 | { 267 | Url: fileUrl, 268 | Resolution: plugin.FileResource_Original, 269 | ResourceType: plugin.FileResource_Video, 270 | }, 271 | }, 272 | }, nil 273 | } 274 | -------------------------------------------------------------------------------- /smb/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/medianexapp/go-smb2" 11 | ) 12 | 13 | func TestSmb(t *testing.T) { 14 | slog.Info("read success") 15 | smbDialer := &smb2.Dialer{ 16 | Initiator: &smb2.NTLMInitiator{ 17 | User: "labulakalia", 18 | Password: "109097", 19 | }, 20 | } 21 | t.Log("start dial") 22 | addr := "192.168.123.213:445" 23 | conn, err := net.Dial("tcp", addr) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | t.Log("dial") 28 | smbSession, err := smbDialer.DialConn(context.Background(), conn, addr) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | t.Log(smbSession.ListSharenames()) 34 | 35 | share, err := smbSession.Mount("labulakalia") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | t.Log(share.ReadDir("")) 40 | slog.Info("read success") 41 | sp := strings.Split("/labulakalia", "/")[1:] 42 | 43 | shareName := sp[0] 44 | t.Log(shareName) 45 | t.Log(strings.Join(sp[1:], "/")) 46 | 47 | } 48 | -------------------------------------------------------------------------------- /smb/smb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/smb/smb.png -------------------------------------------------------------------------------- /util/auth.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | "net/http" 11 | "net/url" 12 | 13 | "github.com/medianexapp/plugin_api/httpclient" 14 | "github.com/medianexapp/plugin_api/plugin" 15 | ) 16 | 17 | //go:embed env.json 18 | var envData []byte 19 | 20 | type Env struct { 21 | ServerAddr string `json:"server_addr"` 22 | } 23 | 24 | func init() { 25 | env := &Env{} 26 | json.Unmarshal(envData, env) 27 | ServerAddr = env.ServerAddr 28 | } 29 | 30 | var ( 31 | ServerAddr = "" 32 | getAuthAddrUri = "/api/get_auth_addr" 33 | getAuthTokenUri = "/api/get_auth_token" 34 | getAuthQrcodeUri = "/api/get_auth_qrcode" 35 | checkAuthQrcodeUri = "/api/check_auth_qrcode" 36 | Client = httpclient.NewClient() 37 | ) 38 | 39 | func GetAuthAddr(pluginId string) string { 40 | return fmt.Sprintf("%s%s?id=%s", ServerAddr, getAuthAddrUri, pluginId) 41 | } 42 | 43 | type GetAuthTokenRequest struct { 44 | Id string `json:"id"` 45 | State string `json:"state"` 46 | Code string `json:"code"` 47 | CodeVerifier string `json:"code_verifier"` 48 | RefreshToken string `json:"refresh_token"` 49 | } 50 | 51 | func GetAuthToken(req *GetAuthTokenRequest) (*plugin.Token, error) { 52 | u := url.Values{} 53 | u.Set("id", req.Id) 54 | u.Set("state", req.State) 55 | u.Set("code", req.Code) 56 | u.Set("code_verifier", req.CodeVerifier) 57 | u.Set("refresh_token", req.RefreshToken) 58 | resp, err := Client.Get(fmt.Sprintf("%s%s?%s", ServerAddr, getAuthTokenUri, u.Encode())) 59 | if err != nil { 60 | err = errors.Unwrap(err) 61 | return nil, err 62 | } 63 | defer resp.Body.Close() 64 | respBytes, err := io.ReadAll(resp.Body) 65 | if err != nil { 66 | return nil, err 67 | } 68 | if resp.StatusCode != http.StatusOK { 69 | return nil, fmt.Errorf("get auth token failed: %s", respBytes) 70 | } 71 | token := &plugin.Token{} 72 | err = token.UnmarshalVT(respBytes) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return token, nil 77 | } 78 | 79 | func GetAuthQrcode(id string) ([]byte, error) { 80 | url := fmt.Sprintf("%s%s?id=%s", ServerAddr, getAuthQrcodeUri, id) 81 | resp, err := Client.Get(url) 82 | if err != nil { 83 | return nil, err 84 | } 85 | defer resp.Body.Close() 86 | respBytes, err := io.ReadAll(resp.Body) 87 | if err != nil { 88 | return nil, err 89 | } 90 | slog.Info("get qrcode data", "id", id, "resp", string(respBytes)) 91 | if resp.StatusCode != http.StatusOK { 92 | return nil, fmt.Errorf("get auth qrcode failed: %s", respBytes) 93 | } 94 | return respBytes, nil 95 | } 96 | 97 | func CheckAuthQrcode(id, key string) (*plugin.Token, error) { 98 | url := fmt.Sprintf("%s%s?id=%s&key=%s", ServerAddr, checkAuthQrcodeUri, id, key) 99 | resp, err := Client.Get(url) 100 | if err != nil { 101 | return nil, err 102 | } 103 | defer resp.Body.Close() 104 | if resp.ContentLength == 0 { 105 | return nil, nil 106 | } 107 | respBytes, err := io.ReadAll(resp.Body) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | slog.Info("get qrcode data", "id", id, "resp", string(respBytes)) 113 | if resp.StatusCode != http.StatusOK { 114 | return nil, fmt.Errorf("get auth qrcode failed: %s", respBytes) 115 | } 116 | token := &plugin.Token{} 117 | err = token.UnmarshalVT(respBytes) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return token, nil 122 | } 123 | -------------------------------------------------------------------------------- /util/auth_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestAuth(t *testing.T) { 9 | type QrcodeResponse struct { 10 | QrCodeUrl string `json:"qrCodeUrl"` 11 | Sid string `json:"sid"` 12 | } 13 | res, err := GetAuthQrcode("alipan") 14 | if err != nil { 15 | t.Errorf("GetAuthQrcode failed: %v", err) 16 | } 17 | qrResp := &QrcodeResponse{} 18 | err = json.Unmarshal(res, qrResp) 19 | if err != nil { 20 | t.Errorf("json.Unmarshal failed: %v", err) 21 | } 22 | token, err := CheckAuthQrcode("alipan", qrResp.Sid) 23 | if err != nil { 24 | t.Fatal("err is nil") 25 | } 26 | if token != nil { 27 | t.Fatal("token is nil") 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /util/env.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /webdav/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /webdav/Makefile: -------------------------------------------------------------------------------- 1 | CHECK_PROGRAM := $(shell which plugin_api 2>/dev/null) 2 | ifeq ($(CHECK_PROGRAM),) 3 | $(error "plugin_api not found,install cmd: go install github.com/medianexapp/plugin_api/cmd/plugin_api@latest") 4 | endif 5 | build: 6 | plugin_api build 7 | -------------------------------------------------------------------------------- /webdav/plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/medianexapp/plugin_api" 5 | ) 6 | 7 | func init() { 8 | plugin_api.RegistryPlugin(NewPluginImpl()) 9 | } 10 | 11 | func main() {} 12 | -------------------------------------------------------------------------------- /webdav/plugin.toml: -------------------------------------------------------------------------------- 1 | id = "webdav" 2 | name = "Webdav" 3 | desc = "webdav driver plugin" 4 | icon = "webdav.png" 5 | author = ["labulakalia(labulakalia@gmail.com)"] 6 | version = "v0.0.2" 7 | changelog = ["webdav plugin init"] 8 | -------------------------------------------------------------------------------- /webdav/plugin_impl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "log/slog" 7 | 8 | "github.com/labulakalia/wazero_net/util" 9 | _ "github.com/labulakalia/wazero_net/wasi/http" 10 | "github.com/medianexapp/gowebdav" 11 | "github.com/medianexapp/plugin_api/httpclient" 12 | "github.com/medianexapp/plugin_api/plugin" 13 | ) 14 | 15 | type PluginImpl struct { 16 | webDavAuth *webDavAuth 17 | 18 | client *gowebdav.Client 19 | httpclient *httpclient.Client 20 | } 21 | 22 | func NewPluginImpl() *PluginImpl { 23 | 24 | return &PluginImpl{ 25 | webDavAuth: &webDavAuth{ 26 | Addr: plugin.String("http://127.0.0.1"), 27 | User: plugin.String(""), 28 | Password: plugin.ObscureString(""), 29 | }, 30 | httpclient: httpclient.NewClient(), 31 | } 32 | } 33 | 34 | type webDavAuth struct { 35 | Addr *plugin.Formdata_FormItem_StringValue 36 | User *plugin.Formdata_FormItem_StringValue 37 | Password *plugin.Formdata_FormItem_ObscureStringValue 38 | } 39 | 40 | // Id implements IPlugin. 41 | func (p *PluginImpl) PluginId() (string, error) { 42 | return "webdav", nil 43 | } 44 | 45 | // GetAuthType implements IPlugin. 46 | func (p *PluginImpl) GetAuth() (*plugin.Auth, error) { 47 | addrValue := p.webDavAuth.Addr 48 | userValue := p.webDavAuth.User 49 | passwordValue := p.webDavAuth.Password 50 | 51 | authMethod := &plugin.AuthMethod{ 52 | Method: &plugin.AuthMethod_Formdata{ 53 | Formdata: &plugin.Formdata{ 54 | FormItems: []*plugin.Formdata_FormItem{ 55 | { 56 | Name: "Addr", 57 | Value: addrValue, 58 | }, 59 | { 60 | Name: "User", 61 | Value: userValue, 62 | }, 63 | { 64 | Name: "Password", 65 | Value: passwordValue, 66 | }, 67 | }, 68 | }, 69 | }, 70 | } 71 | 72 | return &plugin.Auth{ 73 | AuthMethods: []*plugin.AuthMethod{authMethod}, 74 | }, nil 75 | } 76 | 77 | // CheckAuth implements IPlugin. 78 | func (p *PluginImpl) CheckAuthMethod(authMethod *plugin.AuthMethod) (authData *plugin.AuthData, err error) { 79 | formData := authMethod.Method.(*plugin.AuthMethod_Formdata).Formdata 80 | authDataBytes, err := formData.MarshalVT() 81 | if err != nil { 82 | return nil, err 83 | } 84 | authData = &plugin.AuthData{ 85 | AuthDataBytes: authDataBytes, 86 | } 87 | return authData, nil 88 | } 89 | 90 | // InitAuth implements IPlugin. 91 | func (p *PluginImpl) CheckAuthData(authData []byte) error { 92 | formData := &plugin.Formdata{} 93 | err := formData.UnmarshalVT(authData) 94 | if err != nil { 95 | return err 96 | } 97 | p.webDavAuth.Addr.StringValue.Value = formData.FormItems[0].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value 98 | p.webDavAuth.User.StringValue.Value = formData.FormItems[1].Value.(*plugin.Formdata_FormItem_StringValue).StringValue.Value 99 | p.webDavAuth.Password.ObscureStringValue.Value = formData.FormItems[2].Value.(*plugin.Formdata_FormItem_ObscureStringValue).ObscureStringValue.Value 100 | 101 | slog.Debug("webdav connect", "addr", p.webDavAuth.Addr.StringValue.Value, "user", p.webDavAuth.User.StringValue.Value, "passwd", p.webDavAuth.Password.ObscureStringValue.Value) 102 | p.client = gowebdav.NewClient(p.webDavAuth.Addr.StringValue.Value, p.webDavAuth.User.StringValue.Value, p.webDavAuth.Password.ObscureStringValue.Value) 103 | p.client.SetClientDo(p.httpclient.Do) 104 | err = p.client.Connect() 105 | if err != nil { 106 | slog.Error("connect failed", "err", err) 107 | return err 108 | } 109 | _, err = p.client.Stat("/") 110 | return err 111 | } 112 | 113 | // AuthId implements IPlugin. 114 | func (p *PluginImpl) PluginAuthId() (string, error) { 115 | id := fmt.Sprintf("%s%s%s", p.webDavAuth.Addr.StringValue.Value, p.webDavAuth.User.StringValue.Value, p.webDavAuth.Password.ObscureStringValue.Value) 116 | return fmt.Sprintf("%x", md5.Sum(util.StringToBytes(&id))), nil 117 | } 118 | 119 | // GetDirEntry implements IPlugin. 120 | func (p *PluginImpl) GetDirEntry(req *plugin.GetDirEntryRequest) (*plugin.DirEntry, error) { 121 | dirPath := req.Path 122 | page := req.Page 123 | pageSize := req.PageSize 124 | fileInfos, err := p.client.ReadDir(dirPath) 125 | if err != nil { 126 | slog.Error("read dir failed", "err", err, "dir", dirPath, "fileInfos", fileInfos) 127 | return nil, err 128 | } 129 | dirEntry := &plugin.DirEntry{ 130 | FileEntries: make([]*plugin.FileEntry, 0, len(fileInfos)), 131 | } 132 | 133 | start := int((page - 1) * pageSize) 134 | end := start + int(pageSize) 135 | 136 | if len(fileInfos) <= start { 137 | return dirEntry, nil 138 | } else if len(fileInfos) >= end { 139 | fileInfos = fileInfos[start:end] 140 | } else { 141 | fileInfos = fileInfos[start:] 142 | } 143 | for _, fileinfo := range fileInfos { 144 | fileinfo.Sys() 145 | fileEntry := &plugin.FileEntry{ 146 | Name: fileinfo.Name(), 147 | Size: uint64(fileinfo.Size()), 148 | ModifiedTime: uint64(fileinfo.ModTime().Unix()), 149 | AccessedTime: uint64(fileinfo.ModTime().Unix()), 150 | CreatedTime: uint64(fileinfo.ModTime().Unix()), 151 | } 152 | if fileinfo.IsDir() { 153 | fileEntry.FileType = plugin.FileEntry_FileTypeDir 154 | } else { 155 | fileEntry.FileType = plugin.FileEntry_FileTypeFile 156 | } 157 | 158 | dirEntry.FileEntries = append(dirEntry.FileEntries, fileEntry) 159 | } 160 | return dirEntry, nil 161 | } 162 | 163 | // GetFileResource implements IPlugin. 164 | func (p *PluginImpl) GetFileResource(req *plugin.GetFileResourceRequest) (*plugin.FileResource, error) { 165 | _, err := p.client.Stat(req.FilePath) 166 | if err != nil { 167 | return nil, err 168 | } 169 | pathReq, err := p.client.GetPathRequest(req.FilePath) 170 | if err != nil { 171 | return nil, err 172 | } 173 | url := pathReq.URL.String() 174 | header := map[string]string{} 175 | 176 | for k := range pathReq.Header { 177 | header[k] = pathReq.Header.Get(k) 178 | } 179 | return &plugin.FileResource{ 180 | FileResourceData: []*plugin.FileResource_FileResourceData{ 181 | { 182 | Url: url, 183 | Header: header, 184 | ResourceType: plugin.FileResource_Video, 185 | Resolution: plugin.FileResource_Original, 186 | }, 187 | }, 188 | }, nil 189 | } 190 | -------------------------------------------------------------------------------- /webdav/plugin_impl_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/medianexapp/gowebdav" 7 | "github.com/medianexapp/plugin_api/httpclient" 8 | ) 9 | 10 | func TestWebdav(t *testing.T) { 11 | client := gowebdav.NewClient("", "", "") 12 | cc := httpclient.NewClient() 13 | client.SetClientDo(cc.Do) 14 | err := client.Connect() 15 | if err != nil { 16 | 17 | t.Fatal(err) 18 | } 19 | dirs, err := client.Stat("/") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | t.Log(dirs) 24 | path := `/疯狂动物城 4K原盘REMUX 国英双音 内封字幕 默认国音` 25 | 26 | t.Log(client.ReadDir(path)) 27 | } 28 | -------------------------------------------------------------------------------- /webdav/webdav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medianexapp/plugins/a52721471dbe2f48eb9d600f4bea4411a287581a/webdav/webdav.png --------------------------------------------------------------------------------