2021-10-10 10:48:53 +08:00
|
|
|
// Copyright (c) 2020 tickstep.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
2023-10-07 16:42:43 +08:00
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
2021-10-10 10:48:53 +08:00
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
|
|
|
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
2024-03-02 00:55:46 +08:00
|
|
|
"github.com/tickstep/aliyunpan-api/aliyunpan_open"
|
|
|
|
"github.com/tickstep/aliyunpan-api/aliyunpan_open/openapi"
|
2024-03-03 14:02:05 +08:00
|
|
|
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
2024-03-02 00:55:46 +08:00
|
|
|
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
2021-10-30 22:53:08 +08:00
|
|
|
"github.com/tickstep/library-go/expires/cachemap"
|
2021-10-10 10:48:53 +08:00
|
|
|
"github.com/tickstep/library-go/logger"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
2024-03-02 00:55:46 +08:00
|
|
|
"time"
|
2021-10-10 10:48:53 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
type DriveInfo struct {
|
|
|
|
DriveId string `json:"driveId"`
|
|
|
|
DriveName string `json:"driveName"`
|
2022-01-30 22:10:46 +08:00
|
|
|
DriveTag string `json:"driveTag"`
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
|
|
|
type DriveInfoList []*DriveInfo
|
|
|
|
|
2022-01-02 18:44:45 +08:00
|
|
|
func (d DriveInfoList) GetFileDriveId() string {
|
2022-01-30 22:10:46 +08:00
|
|
|
for _, drive := range d {
|
2022-01-02 18:44:45 +08:00
|
|
|
if drive.DriveTag == "File" {
|
|
|
|
return drive.DriveId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2022-02-08 19:34:22 +08:00
|
|
|
func (d DriveInfoList) GetAlbumDriveId() string {
|
|
|
|
for _, drive := range d {
|
|
|
|
if drive.DriveTag == "Album" {
|
|
|
|
return drive.DriveId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-10-07 16:42:43 +08:00
|
|
|
func (d DriveInfoList) GetResourceDriveId() string {
|
|
|
|
for _, drive := range d {
|
|
|
|
if drive.DriveTag == "Resource" {
|
|
|
|
return drive.DriveId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// PanClientToken 授权Token
|
|
|
|
type PanClientToken struct {
|
|
|
|
// AccessToken AccessToken
|
|
|
|
AccessToken string `json:"accessToken"`
|
|
|
|
// Expired 过期时间戳,单位秒
|
|
|
|
Expired int64 `json:"expired"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetExpiredTimeCstStr 获取东八区时间字符串
|
|
|
|
func (t *PanClientToken) GetExpiredTimeCstStr() string {
|
|
|
|
cz := time.FixedZone("CST", 8*3600) // 东8区
|
|
|
|
tm := time.Unix(t.Expired, 0).In(cz)
|
|
|
|
return tm.Format("2006-01-02 15:04:05")
|
|
|
|
}
|
|
|
|
|
2021-10-10 10:48:53 +08:00
|
|
|
type PanUser struct {
|
2024-03-02 00:55:46 +08:00
|
|
|
// 用户信息
|
2021-10-10 10:48:53 +08:00
|
|
|
UserId string `json:"userId"`
|
2022-01-30 22:10:46 +08:00
|
|
|
Nickname string `json:"nickname"`
|
2021-10-10 10:48:53 +08:00
|
|
|
AccountName string `json:"accountName"`
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// 文件(备份盘)
|
2022-01-30 22:10:46 +08:00
|
|
|
Workdir string `json:"workdir"`
|
2021-10-10 10:48:53 +08:00
|
|
|
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
|
|
|
|
|
2023-10-07 16:42:43 +08:00
|
|
|
// 资源库
|
|
|
|
ResourceWorkdir string `json:"resourceWorkdir"`
|
|
|
|
ResourceWorkdirFileEntity aliyunpan.FileEntity `json:"resourceWorkdirFileEntity"`
|
|
|
|
|
|
|
|
// 相册
|
2022-01-30 22:10:46 +08:00
|
|
|
AlbumWorkdir string `json:"albumWorkdir"`
|
2021-10-10 10:48:53 +08:00
|
|
|
AlbumWorkdirFileEntity aliyunpan.FileEntity `json:"albumWorkdirFileEntity"`
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// 用户网盘信息
|
2022-01-30 22:10:46 +08:00
|
|
|
ActiveDriveId string `json:"activeDriveId"`
|
|
|
|
DriveList DriveInfoList `json:"driveList"`
|
2021-10-10 10:48:53 +08:00
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// 授权Token信息
|
|
|
|
TicketId string `json:"ticketId"`
|
|
|
|
WebapiToken *PanClientToken `json:"webapiToken"`
|
|
|
|
OpenapiToken *PanClientToken `json:"openapiToken"`
|
2021-10-10 10:48:53 +08:00
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// API客户端
|
|
|
|
panClient *PanClient `json:"-"`
|
|
|
|
cacheOpMap cachemap.CacheOpMap `json:"-"`
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
type PanUserList []*PanUser
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
func SetupUserByCookie(openapiToken, webapiToken *PanClientToken, ticketId, userId, deviceId, deviceName, clientId, clientSecret string) (user *PanUser, err *apierror.ApiError) {
|
2021-10-10 10:48:53 +08:00
|
|
|
tryRefreshWebToken := true
|
2024-03-02 00:55:46 +08:00
|
|
|
tryRefreshOpenToken := true
|
|
|
|
loginHelper := panlogin.NewLoginHelper(DefaultTokenServiceWebHost)
|
2021-10-10 10:48:53 +08:00
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
if openapiToken == nil {
|
|
|
|
return nil, apierror.NewFailedApiError("openapi token is empty")
|
|
|
|
}
|
2024-04-01 22:46:24 +08:00
|
|
|
//if webapiToken == nil {
|
|
|
|
// return nil, apierror.NewFailedApiError("webapi token is empty")
|
|
|
|
//}
|
2024-03-02 00:55:46 +08:00
|
|
|
|
|
|
|
doOpenLoginAct:
|
|
|
|
// setup openapi client
|
|
|
|
openPanClient := aliyunpan_open.NewOpenPanClient(openapi.ApiConfig{
|
|
|
|
TicketId: ticketId,
|
|
|
|
UserId: userId,
|
|
|
|
ClientId: clientId,
|
|
|
|
ClientSecret: clientSecret,
|
|
|
|
}, openapi.ApiToken{
|
|
|
|
AccessToken: openapiToken.AccessToken,
|
|
|
|
ExpiredAt: openapiToken.Expired,
|
2024-03-02 22:10:54 +08:00
|
|
|
}, nil)
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// open api token maybe expired
|
|
|
|
// check & refresh new one
|
|
|
|
openUserInfo, err := openPanClient.GetUserInfo()
|
|
|
|
if err != nil {
|
|
|
|
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken {
|
|
|
|
tryRefreshOpenToken = false
|
2024-03-03 09:11:58 +08:00
|
|
|
wt, e := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
|
2024-03-02 00:55:46 +08:00
|
|
|
if e != nil {
|
|
|
|
logger.Verboseln("get openapi token from server error: ", e)
|
|
|
|
return nil, apierror.NewFailedApiError("get new openapi token error, try login again")
|
|
|
|
}
|
|
|
|
if wt != nil {
|
|
|
|
openapiToken = &PanClientToken{
|
|
|
|
AccessToken: wt.AccessToken,
|
|
|
|
Expired: wt.Expired,
|
|
|
|
}
|
|
|
|
}
|
2024-03-02 21:22:02 +08:00
|
|
|
time.Sleep(time.Duration(1) * time.Second)
|
|
|
|
goto doOpenLoginAct
|
2024-03-02 00:55:46 +08:00
|
|
|
}
|
|
|
|
return nil, err
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
doWebLoginAct:
|
|
|
|
// setup webapi client
|
2024-03-02 21:22:02 +08:00
|
|
|
var webUserInfo *aliyunpan.UserInfo
|
|
|
|
var err2 *apierror.ApiError
|
2024-03-03 14:07:35 +08:00
|
|
|
var webPanClient *aliyunpan_web.WebPanClient
|
2024-03-02 21:22:02 +08:00
|
|
|
if webapiToken != nil && webapiToken.AccessToken != "" {
|
2024-03-03 14:02:05 +08:00
|
|
|
appConfig := aliyunpan_web.AppConfig{
|
2024-03-02 21:22:02 +08:00
|
|
|
AppId: "25dzX3vbYqktVxyX",
|
|
|
|
DeviceId: deviceId,
|
|
|
|
UserId: userId,
|
|
|
|
Nonce: 0,
|
|
|
|
PublicKey: "",
|
|
|
|
}
|
2024-03-03 14:07:35 +08:00
|
|
|
webPanClient = aliyunpan_web.NewWebPanClient(aliyunpan_web.WebLoginToken{
|
2024-03-02 21:22:02 +08:00
|
|
|
AccessTokenType: "Bearer",
|
|
|
|
AccessToken: webapiToken.AccessToken,
|
|
|
|
RefreshToken: "",
|
|
|
|
ExpiresIn: 7200,
|
|
|
|
ExpireTime: webapiToken.GetExpiredTimeCstStr(),
|
2024-03-03 14:02:05 +08:00
|
|
|
}, aliyunpan_web.AppLoginToken{}, appConfig, aliyunpan_web.SessionConfig{
|
2024-03-02 21:22:02 +08:00
|
|
|
DeviceName: deviceName,
|
|
|
|
ModelName: "Windows网页版",
|
|
|
|
})
|
|
|
|
// web api token maybe expired
|
|
|
|
// check & refresh new one
|
|
|
|
webUserInfo, err2 = webPanClient.GetUserInfo()
|
|
|
|
if err2 != nil {
|
|
|
|
if err2.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshWebToken {
|
|
|
|
tryRefreshWebToken = false
|
2024-03-03 09:11:58 +08:00
|
|
|
wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken)
|
2024-03-02 21:22:02 +08:00
|
|
|
if e != nil {
|
|
|
|
logger.Verboseln("get web token from server error: ", e)
|
2024-03-02 00:55:46 +08:00
|
|
|
}
|
2024-03-02 21:22:02 +08:00
|
|
|
if wt != nil {
|
|
|
|
webapiToken = &PanClientToken{
|
|
|
|
AccessToken: wt.AccessToken,
|
|
|
|
Expired: wt.Expired,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
time.Sleep(time.Duration(1) * time.Second)
|
2024-03-02 00:55:46 +08:00
|
|
|
goto doWebLoginAct
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
2024-03-02 21:22:02 +08:00
|
|
|
webPanClient = nil
|
|
|
|
//return nil, err2
|
|
|
|
}
|
|
|
|
// web create session
|
|
|
|
if webUserInfo != nil {
|
|
|
|
appConfig.UserId = webUserInfo.UserId
|
|
|
|
webPanClient.UpdateAppConfig(appConfig)
|
|
|
|
r, e := webPanClient.CreateSession(nil)
|
|
|
|
if e != nil {
|
|
|
|
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
|
|
|
|
}
|
|
|
|
if r != nil && !r.Result {
|
|
|
|
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
|
|
|
|
}
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
2024-03-02 00:55:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// setup PanUser
|
|
|
|
//
|
|
|
|
u := &PanUser{
|
|
|
|
WebapiToken: webapiToken,
|
|
|
|
OpenapiToken: openapiToken,
|
|
|
|
panClient: NewPanClient(webPanClient, openPanClient),
|
|
|
|
Workdir: "/",
|
|
|
|
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
|
|
|
|
}
|
2024-03-02 22:10:54 +08:00
|
|
|
u.PanClient().OpenapiPanClient().SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
|
|
|
|
logger.Verboseln("openapi token refresh, update for user")
|
|
|
|
u.OpenapiToken = &PanClientToken{
|
|
|
|
AccessToken: newToken.AccessToken,
|
|
|
|
Expired: newToken.ExpiredAt,
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
2024-03-02 00:55:46 +08:00
|
|
|
|
|
|
|
// setup user info
|
2021-10-10 10:48:53 +08:00
|
|
|
name := "Unknown"
|
2024-03-02 00:55:46 +08:00
|
|
|
if openUserInfo != nil {
|
|
|
|
// fill userId for client
|
|
|
|
u.PanClient().OpenapiPanClient().UpdateUserId(openUserInfo.UserId)
|
2024-03-02 21:22:02 +08:00
|
|
|
if u.PanClient().WebapiPanClient() != nil {
|
|
|
|
u.PanClient().WebapiPanClient().UpdateUserId(openUserInfo.UserId)
|
|
|
|
}
|
2021-10-10 10:48:53 +08:00
|
|
|
|
|
|
|
// update user
|
2024-03-02 00:55:46 +08:00
|
|
|
if openUserInfo.Nickname != "" {
|
|
|
|
name = openUserInfo.Nickname
|
|
|
|
}
|
|
|
|
u.UserId = openUserInfo.UserId
|
2021-10-10 10:48:53 +08:00
|
|
|
u.Nickname = name
|
2024-03-02 00:55:46 +08:00
|
|
|
u.AccountName = openUserInfo.UserName
|
2021-10-10 10:48:53 +08:00
|
|
|
|
|
|
|
// default file drive
|
|
|
|
if u.ActiveDriveId == "" {
|
2024-03-02 00:55:46 +08:00
|
|
|
u.ActiveDriveId = openUserInfo.FileDriveId
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// drive list
|
|
|
|
u.DriveList = DriveInfoList{
|
2024-03-02 00:55:46 +08:00
|
|
|
{DriveId: openUserInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
|
|
|
|
{DriveId: openUserInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
|
|
|
|
}
|
|
|
|
if webUserInfo != nil {
|
|
|
|
u.AccountName = webUserInfo.UserName
|
|
|
|
u.DriveList = append(u.DriveList, &DriveInfo{DriveId: webUserInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"})
|
2021-10-10 10:48:53 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// error, maybe the token has expired
|
2024-03-02 00:55:46 +08:00
|
|
|
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired. please login again")
|
2023-02-16 22:25:09 +08:00
|
|
|
}
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
// return user
|
2021-10-10 10:48:53 +08:00
|
|
|
return u, nil
|
|
|
|
}
|
|
|
|
|
2024-03-02 00:55:46 +08:00
|
|
|
func (pu *PanUser) PanClient() *PanClient {
|
2021-10-10 10:48:53 +08:00
|
|
|
return pu.panClient
|
|
|
|
}
|
|
|
|
|
2024-03-03 23:20:27 +08:00
|
|
|
func (pu *PanUser) UpdateClient(openClient *aliyunpan_open.OpenPanClient, webClient *aliyunpan_web.WebPanClient) {
|
|
|
|
if pu.panClient == nil {
|
|
|
|
pu.panClient = &PanClient{
|
|
|
|
webapiPanClient: webClient,
|
|
|
|
openapiPanClient: openClient,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
pu.panClient.webapiPanClient = webClient
|
|
|
|
pu.panClient.openapiPanClient = openClient
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-10 10:48:53 +08:00
|
|
|
// PathJoin 合并工作目录和相对路径p, 若p为绝对路径则忽略
|
|
|
|
func (pu *PanUser) PathJoin(driveId, p string) string {
|
|
|
|
if path.IsAbs(p) {
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
wd := "/"
|
|
|
|
di := pu.GetDriveById(driveId)
|
|
|
|
if di != nil {
|
|
|
|
if di.IsFileDrive() {
|
|
|
|
wd = pu.Workdir
|
2023-10-07 16:42:43 +08:00
|
|
|
} else if di.IsResourceDrive() {
|
|
|
|
wd = pu.ResourceWorkdir
|
2021-10-10 10:48:53 +08:00
|
|
|
} else if di.IsAlbumDrive() {
|
|
|
|
wd = pu.AlbumWorkdir
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return path.Join(wd, p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pu *PanUser) FreshWorkdirInfo() {
|
|
|
|
if pu.IsFileDriveActive() {
|
2024-03-02 17:38:21 +08:00
|
|
|
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.WorkdirFileEntity.FileId)
|
2021-10-10 10:48:53 +08:00
|
|
|
if err != nil {
|
|
|
|
logger.Verboseln("刷新工作目录信息失败")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pu.WorkdirFileEntity = *fe
|
|
|
|
} else if pu.IsAlbumDriveActive() {
|
2024-03-02 17:38:21 +08:00
|
|
|
fe, err := pu.PanClient().OpenapiPanClient().FileInfoById(pu.ActiveDriveId, pu.AlbumWorkdirFileEntity.FileId)
|
2021-10-10 10:48:53 +08:00
|
|
|
if err != nil {
|
|
|
|
logger.Verboseln("刷新工作目录信息失败")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
pu.AlbumWorkdirFileEntity = *fe
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetSavePath 根据提供的网盘文件路径 panpath, 返回本地储存路径,
|
|
|
|
// 返回绝对路径, 获取绝对路径出错时才返回相对路径...
|
|
|
|
func (pu *PanUser) GetSavePath(filePanPath string) string {
|
|
|
|
dirStr := filepath.Join(Config.SaveDir, fmt.Sprintf("%s", pu.UserId), filePanPath)
|
|
|
|
dir, err := filepath.Abs(dirStr)
|
|
|
|
if err != nil {
|
|
|
|
dir = filepath.Clean(dirStr)
|
|
|
|
}
|
|
|
|
return dir
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pu *PanUser) GetDriveByTag(tag string) *DriveInfo {
|
2022-01-30 22:10:46 +08:00
|
|
|
for _, item := range pu.DriveList {
|
2021-10-10 10:48:53 +08:00
|
|
|
if item.DriveTag == tag {
|
|
|
|
return item
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pu *PanUser) GetDriveById(id string) *DriveInfo {
|
2022-01-30 22:10:46 +08:00
|
|
|
for _, item := range pu.DriveList {
|
2021-10-10 10:48:53 +08:00
|
|
|
if item.DriveId == id {
|
|
|
|
return item
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pu *PanUser) GetActiveDriveInfo() *DriveInfo {
|
2022-01-30 22:10:46 +08:00
|
|
|
for _, item := range pu.DriveList {
|
2021-10-10 10:48:53 +08:00
|
|
|
if item.DriveId == pu.ActiveDriveId {
|
|
|
|
return item
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pu *PanUser) IsFileDriveActive() bool {
|
|
|
|
d := pu.GetActiveDriveInfo()
|
|
|
|
return d != nil && d.IsFileDrive()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pu *PanUser) IsAlbumDriveActive() bool {
|
|
|
|
d := pu.GetActiveDriveInfo()
|
|
|
|
return d != nil && d.IsAlbumDrive()
|
|
|
|
}
|
|
|
|
|
2023-10-07 16:42:43 +08:00
|
|
|
func (pu *PanUser) IsResourceDriveActive() bool {
|
|
|
|
d := pu.GetActiveDriveInfo()
|
|
|
|
return d != nil && d.IsResourceDrive()
|
|
|
|
}
|
|
|
|
|
2021-10-10 10:48:53 +08:00
|
|
|
func (di *DriveInfo) IsFileDrive() bool {
|
|
|
|
return di.DriveTag == "File"
|
|
|
|
}
|
|
|
|
|
|
|
|
func (di *DriveInfo) IsAlbumDrive() bool {
|
|
|
|
return di.DriveTag == "Album"
|
2022-01-30 22:10:46 +08:00
|
|
|
}
|
2023-10-07 16:42:43 +08:00
|
|
|
|
|
|
|
func (di *DriveInfo) IsResourceDrive() bool {
|
|
|
|
return di.DriveTag == "Resource"
|
|
|
|
}
|