Skip to main content

简介#

智汀云盘 是一款在 Swift 5 中开发的 iOS 云盘APP。该应用一直在积极升级以采用 iOS 和 Swift 语言的最新功能。此应用配合智汀家庭云这款智能家居APP授权使用。

1. 快速上手#

1.1 开发工具#

  • 当前版本适用于 Xcode 版本 Xcode 13 。如果您使用不同的 Xcode 版本,请查看之前的版本。
  • 此版本为仅使用 Swift 5 支持 iOS 13+。

1.2 源码地址#

  1. Git Hub

    名称URL描述
    zhiting-nas-ioshttps://github.com/zhiting-tech/sa-ios-sdkiOS源码
  2. gitee

    名称URL描述
    zhiting-nas-ioshttps://gitee.com/zhiting-tech/sa-ios-sdkiOS源码

1.3 构建版本#

  • 克隆存储库

    bash $ git clone https://xxxx/sa-ios-sdk.git
  • CocoaPods

    安装CocoaPods,详情可查询CocoaPods

    CocoaPods安装完成后,请用CocoaPods下载第三方库

    $ cd sa-ios-sdk$ pod install

    如遇下载失败,则可能需科学上网,自行配置网络代理。

  • 运行程序

    pod install 成功后,在模拟器中编译并运行应用程序。

    如果您没有看到任何数据,请检查 "Simulator" -> "Debug" -> "Location" 以更改位置。

2. 开发指南#

2.1 设计模式#

本项目采用MVC设计模式

2.2 组织架构#

项目组织架构如下图

2.3 授权登录#

登录授权页面点击按钮后通过URLSchema的方式唤醒智汀家庭云App进行授权

loginBtn.clickCallBack = { [weak self] _ in    guard let self = self else { return }    if let url = URL(string: "zhiting://operatioaction=diskAuth"), UIApplication.shared.canOpenU(url) {        print("跳转成功")        UIApplication.shared.open(url, options: [:]completionHandler: nil)     } else {        self.showToast("请先安装 \"智汀家庭云\" APP")        print("跳转失败")    }}

智汀家庭云授权部分代码片段:

    /// 授权当前绑定SA的家庭    private func requestAreaScopeToken() {        let scopes = authItems.filter({ $0.isSelected }).map(\.name)        if !authManager.currentArea.is_bind_sa {            showToast(string: "当前家庭未绑定SA".localizedString)            confirmButton.selectedChangeView(isLoading: false)            return        }
        ApiServiceManager.shared.scopeToken(area: authManager.currentArea, scopes: scopes) { [weak self] response in            guard let self = self else { return }            let area = self.transferToAuthedArea(from: self.authManager.currentArea, scopeTokenModel: response.scope_token)            if self.authManager.isLogin {                self.returnAuthResult(cloud_user_id: self.authManager.currentArea.cloud_user_id, cloud_url: cloudUrl, areas: [area])            } else {                area.id = 0                self.returnAuthResult(cloud_user_id: nil, cloud_url: nil, areas: [area])            }        } failureCallback: { [weak self] code, err in            self?.showToast(string: err)            self?.confirmButton.selectedChangeView(isLoading: false)        }    }    private func returnAuthResult(cloud_user_id: Int?, cloud_url: String?, areas: [AuthedAreaModel]) {        let result = ResultModel()        result.cloud_url = cloud_url        result.cloud_user_id = cloud_user_id        result.nickname = authManager.currentUser.nickname        result.areas = areas        if let cloudUrl = cloud_url, let cookie = HTTPCookieStorage.shared.cookies?.first(where: { cloudUrl.contains($0.domain) }) {            result.sessionCookie = cookie.value        }        guard let json = result.toJSONString(),              let data = try? NSKeyedArchiver.archivedData(withRootObject: json, requiringSecureCoding: true)        else {            confirmButton.selectedChangeView(isLoading: false)            return        }        try? data.write(to: shareTokenURL, options: .atomic)                confirmButton.selectedChangeView(isLoading: false)                if let url = URL(string: "zhitingcloud://operation?action=auth") {            UIApplication.shared.open(url, options: [:], completionHandler: nil)        }        dismiss(animated: true, completion: nil)    }

智汀App授权成功后将结果归档并写入到AppGroup共享空间的shareToken.plist文件中,然后通过URLSchema的方式唤醒智汀网盘App,智汀网盘App解析URLSchema,判断是授权成功响应时获取AppGroup共享空间的shareToken.plist文件内容并进行解档得到授权的家庭、用户和云端账号cookies等信息,储存信息并跳转页面.(详情见OpenUrlManager类)

OpenUrlManager类:

import Foundation
// MARK: - OpenUrlManagerclass OpenUrlManager {    enum Action: String {        /// 网盘授权        case auth    }
    static var shared = OpenUrlManager()
    private init() {}
    func open(url: URL) {        let urlString = url.absoluteString           print("--------------------- open from other app ----------------------------------")        print(Date())        print("---------------------------------------------------------------------------")        print("open url from \(urlString)")        print("---------------------------------------------------------------------------\n\n")        guard            let components = urlString.components(separatedBy: "zhitingcloud://operation?").last,            let action = components.components(separatedBy: "&").first?.components(separatedBy: "=").last        else {            return        }          switch Action(rawValue: action) {        case .auth:            // zhitingcloud://operation?action=auth            let shareTokenURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.zhiting.tech")!.appendingPathComponent("shareToken.plist")                        /// 读取授权成功响应并解密            guard                let data = try? Data(contentsOf: shareTokenURL),                let json = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? String            else { return }                        if let authResult = AuthModel.deserialize(from: json) {                let user = User()                user.nickname = authResult.nickname
                /// 存储家庭信息                AreaManager.shared.cacheAreas(areas: authResult.areas)                if let area = AreaManager.shared.getAreaList().first {                    AreaManager.shared.currentArea = area                    user.user_id = area.sa_user_id                }                if let cloudUserId = authResult.cloud_user_id,                   let cloudUrl = authResult.cloud_url,                   let cookieValue = authResult.sessionCookie { /// 云端的授权                    user.user_id = cloudUserId                    UserManager.shared.isCloudUser = true                    UserManager.shared.cloudUrl = cloudUrl
                    /// 写入云端账号cookie                    if let cookie = HTTPCookie(properties: [                        HTTPCookiePropertyKey.domain : cloudUrl,                        HTTPCookiePropertyKey.value : cookieValue,                        HTTPCookiePropertyKey.path : "/",                        HTTPCookiePropertyKey.name : "_session_"                    ]) {                        HTTPCookieStorage.shared.setCookie(cookie)                    }                                                        }                      UserManager.shared.currentUser = user                UserManager.shared.cacheUser(user: user)            }            SceneDelegate.shared.window?.rootViewController = TabbarController()        default:            break        }    }}

2.4 文件列表及权限说明#

2.4.1 文件列表#

2.4.1.1 我的文件——MyFileViewController.swift#
  • 数据获取——通过SA接口获取数据
        NetworkManager.shared.fileList(path: "/", page: page, page_size: 30) { [weak self] response in            guard let self = self else { return }            LoadingView.hide()            self.tableView.mj_header?.endRefreshing()            self.tableView.mj_footer?.endRefreshing()                //删选没有可读权限的文件            let datas = response.list.filter({$0.read != 0})                        if isReload {//下拉刷新 or 首次加载数据                if datas.count == 0 {                    self.tableView.mj_footer?.isHidden = true                    self.tableView.reloadData()                    }else{                        self.emptyView.removeFromSuperview()                        self.currentDatas = datas                        self.tableView.reloadData()                    }            }else{//上拉加载更多数据                if !response.pager.has_more {//已无数据                    self.tableView.mj_footer?.endRefreshingWithNoMoreData()                    self.isGetAllData = true                    return                }                self.isGetAllData = false                self.currentDatas += datas                self.tableView.reloadData()            }        } failureCallback: {[weak self] code, err in            guard let self = self else { return }            LoadingView.hide()            self.tableView.mj_header?.endRefreshing()            self.tableView.mj_footer?.endRefreshing()                        if self.currentDatas.count == 0 {                self.tableView.addSubview(self.emptyView)                    self.emptyView.snp.makeConstraints {                        $0.center.equalToSuperview()                        $0.width.equalTo(Screen.screenWidth)                        $0.height.equalTo(ZTScaleValue(110))                    }                }else{                    self.emptyView.removeFromSuperview()                }            self.showToast("\(err)")        }
  • 此页面无选择功能
  • 点击文件夹进入自己目录
            let vc = ChangeFolderPlaceController()            let nav = UINavigationController(rootViewController: vc)            nav.modalPresentationStyle = .fullScreen            self.present(nav, animated: true, completion: nil)
2.4.1.2 共享文件——ShareFileViewController.swift#
  

用户可以获取到其他用户共享出来的文件夹.

/// 请求列表数据private func getDiskData(){        let page = (currentDatas.count / 30) + 1                NetworkManager.shared.shareFileList(page: page, page_size: 30) { [weak self] response in            guard let self = self else {return}            print("请求成功")            LoadingView.hide()            self.tableView.mj_header?.endRefreshing()            self.tableView.mj_footer?.endRefreshing()                        //删选没有可读权限的文件            let datas = response.list.filter({$0.read != 0})            self.currentDatas.append(contentsOf: datas)                                    if !response.pager.has_more {                self.tableView.mj_footer?.endRefreshingWithNoMoreData()            }
            if self.currentDatas.count == 0 {                //空数据展示页面                self.tableView.addSubview(self.emptyView)                self.emptyView.snp.makeConstraints {                    $0.center.equalToSuperview()                    $0.width.equalTo(Screen.screenWidth)                    $0.height.equalTo(ZTScaleValue(110))                }                self.tableView.reloadData()            }else{                                self.emptyView.removeFromSuperview()                self.tableView.reloadData()            }
        } failureCallback: {[weak self] code, err in            guard let self = self else { return }            print("请求失败")            LoadingView.hide()            self.tableView.mj_header?.endRefreshing()
            if self.currentDatas.count == 0 {                //空数据展示页面                self.tableView.addSubview(self.emptyView)                    self.emptyView.snp.makeConstraints {                        $0.center.equalToSuperview()                        $0.width.equalTo(Screen.screenWidth)                        $0.height.equalTo(ZTScaleValue(110))                    }                }else{                    self.emptyView.removeFromSuperview()                }            self.showToast("\(err)")        }    }
  • 共享文件与我的文件页面逻辑大致相同,但其他用户共享过来的文件夹允许选择功能模块
        // MARK: - funtionTabbarAction        funtionTabbarView.shareBtn.clickCallBack = { _ in            print("点击分享到")            let shareVC = FileShareController()            shareVC.fileDatas = self.seletedFiles            self.navigationController?.pushViewController(shareVC, animated: true)            self.hideFunctionTabbarView()        }        funtionTabbarView.downloadBtn.clickCallBack = { _ in            print("点击下载")        }        funtionTabbarView.copyBtn.clickCallBack = {[weak self] _ in            guard let self = self else {return}            print("点击复制到")            let vc = ChangeFolderPlaceController()            vc.isRootPath = true            vc.currentPaths = ["根目录"]            vc.seletedFiles = self.seletedFiles            vc.type = .copy            let nav = UINavigationController(rootViewController: vc)            nav.modalPresentationStyle = .fullScreen            self.present(nav, animated: true, completion: nil)            self.hideAllFuntionView()        }        funtionTabbarView.resetNameBtn.clickCallBack = { [weak self] _ in            guard let self = self else { return }            print("点击重命名")            guard let file = self.seletedFiles.first else { return }            self.showResetNameView(name: file.name, isFile: file.type == 1)            self.setNameView?.setNameCallback = { name in                if name.isEmpty {                    SceneDelegate.shared.window?.makeToast("请输入名称".localizedString)                    return                }                if let originalExtension = file.name.components(separatedBy: ".").last,                   let newExtension = name.components(separatedBy: ".").last                {                    if originalExtension != newExtension && file.type == 1 {                        let alertViewController = UIAlertController(title: "", message: "更改文件类型可能导致文件不可用,是否继续?", preferredStyle: .alert)                        alertViewController.addAction(UIAlertAction(title: "取消".localizedString, style: .cancel, handler: nil))                        alertViewController.addAction(UIAlertAction(title: "确定".localizedString, style: .default, handler: { [weak self] _ in                            guard let self = self else { return }                            LoadingView.show()                                                        NetworkManager.shared.renameFile(path: file.path, name: name) { [weak self] response in                                guard let self = self else { return }                                file.name = name                                self.setNameView?.removeFromSuperview()                                self.hideAllFuntionView()                                self.tableView.reloadData()                                LoadingView.hide()                                SceneDelegate.shared.window?.makeToast("重命名成功".localizedString)                            } failureCallback: { code, err in                                SceneDelegate.shared.window?.makeToast(err)                                LoadingView.hide()                            }                        }))                        self.present(alertViewController, animated: true, completion: nil)                        return                    }                }
2.4.1.3 文件夹子集目录——FolderViewController.swift#
  1. 子集目录可存在各类型文件的存在形式,根据数据modeltype来区分。
  2. 子集目录点击文件类型是,展示functionView,功能根据权限开放。
  3. 子集目录文件及文件夹均可选择,展示功能tabbar

2.4.2 文件夹共享#

用户可以将文件夹共享给选中的用户,并根据自身需求赋予他们读写删的权限.

    /// 共享文件夹给用户    private func shareFilesToCloud() {                var filePaths = [String]()        for file in fileDatas {            filePaths.append(file.path)        }                if seletedUsers.count == 0 {            return        }        let userIds = seletedUsers.map(\.user_id)        let editMemberAlert = EditShareMemberAlert()        editMemberAlert.set(members: seletedUsers)        editMemberAlert.reSetBtn()        //权限判断        if self.fileDatas.filter({ $0.read == 0 }).count > 0 {//没有可读权限            editMemberAlert.readBtn.isUserInteractionEnabled = false            editMemberAlert.readBtn.alpha = 0.5        } else {            editMemberAlert.readBtn.isUserInteractionEnabled = true            editMemberAlert.readBtn.alpha = 1            editMemberAlert.readBtn.isSelected = true        }        if self.fileDatas.filter({ $0.write == 0 }).count > 0 {//没有写入权限            editMemberAlert.writeBtn.isUserInteractionEnabled = false            editMemberAlert.writeBtn.alpha = 0.5        } else {            editMemberAlert.writeBtn.isUserInteractionEnabled = true            editMemberAlert.writeBtn.alpha = 1        }        if self.fileDatas.filter({ $0.deleted == 0 }).count > 0 {//没有删权限            editMemberAlert.deleteBtn.isUserInteractionEnabled = false            editMemberAlert.deleteBtn.alpha = 0.5        } else {            editMemberAlert.deleteBtn.isUserInteractionEnabled = true            editMemberAlert.deleteBtn.alpha = 1        }        editMemberAlert.sureCallback = { [weak self] read, write, delete in            guard let self = self else { return }            LoadingView.show()            NetworkManager.shared.shareFiles(paths: filePaths, usersId: userIds, read: read, write: write, delete: delete, fromUser: UserManager.shared.currentUser.nickname) { respond in                SceneDelegate.shared.window?.makeToast("共享成功".localizedString)                editMemberAlert.removeFromSuperview()                LoadingView.hide()                self.navigationController?.popViewController(animated: true)                            } failureCallback: { code, err in                LoadingView.hide()                SceneDelegate.shared.window?.makeToast("共享失败".localizedString)            }        }        SceneDelegate.shared.window?.addSubview(editMemberAlert)     }

2.4.3 文件上传下载#

2.4.3.1 文件上传选择器#
    

文件上传选择器中图片和视频的选择器采用的是第三方库TZImagePickerController,详细介绍及使用方法可参考TZImagePickerController,其他文件的选择器则采用系统自带的UIDocumentPickerViewController.

2.4.3.2 文件上传下载功能#
  

智汀云盘App的上传、下载功能是调用gomobile封装的库实现的.该库实现了文件的分片上传、分片下载、断点续传等功能,可以很方便的给客户端调用.iOS端基于该库封装了GoFileManager类,方便项目中使用.

GoFileManager类代码片段:

    /// 下载任务    /// - Parameters:    ///   - url: 任务url    func download(path: String) {        goOperationQueue.async { [weak self] in            guard let self = self else { return }            print(Thread.current)            /// 家庭scopeToken            let scopeToken = AreaManager.shared.currentArea.scope_token                        /// 文件夹密码            let pwd = self.getDirDownloadPwd(by: path)
            let downloader = GonetGetFileDownloader(self.downloadUrlFromPath(path), self.goCachePath, "{\"scope-token\":\"\(scopeToken)\", \"pwd\":\"\(pwd)\"}")            self.taskCountChangePublisher.send(())                        if self.getDownloadingCount() < 2 {                downloader?.start(self.goDownloadCallbackObj)            }                    }     }    /// 上传文件    /// - Parameters:    ///   - urlPath: 上传地址    ///   - filename: 文件名    ///   - tmpPath: 文件位置(图片、视频为绝对路径  其他文件为沙盒tmp下相对路径)    ///   - scopeToken: scopeToken    func upload(urlPath: String, filename: String, tmpPath: String) {        goOperationQueue.async { [weak self] in            guard let self = self else { return }                        var filePath = tmpPath            if let path = tmpPath.components(separatedBy: "file://").last {                filePath = path            }                        let scopeToken = AreaManager.shared.currentArea.scope_token            let pwd = self.getDirUploadPwd(by: urlPath)
            let uploader = GonetGetFileUploader("\(urlPath)\(filename)", self.goCachePath, filePath, filename, "{\"scope-token\":\"\(scopeToken)\", \"pwd\":\"\(pwd)\"}")            try? uploader?.createTmpFile()            self.taskCountChangePublisher.send(())                                    if self.getUploadingCount() < 2 {                uploader?.start(self.goUploadCallbackObj)            }          }    }

2.4.4 权限说明#

  • :model类型的read为0则无可读权限,为1则有可读权限。

    1、查看该文件夹及其下面所有文件/文件夹的权限; 2、有权限才展示对应文件夹的入口

  • :model类型的write为0则无可写权限,为1则有可写权限。

    1、包括新建文件夹、上传、重命名、共享、下载的权限; 2、有权限才展示对应操作的入口;

  • :model类型的delete为0则无可删权限,为1则有可删权限。

    1、包括移动、删除的权限;

    2、有权限才展示对应的操作的入口;

    3、有移动、复制的权限,不代表能成功移动复制,需要看是否有移入文件夹的【写】权限,有才能操作;

2.5 文件夹加密逻辑#

2.5.1 加密文件夹—加密过程#

1.文件夹的创建可设置私人文件夹及加密过程:

2.访问服务器接口,创建私人文件夹

            NetworkManager.shared.createFolder(name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: is_encrypt, pwd: pwd, confirm_pwd: confirmPwd, mode: mode, auth: members) { [weak self] _ in                self?.showToast("保存成功".localizedString)                self?.navigationController?.popViewController(animated: true)
            } failureCallback: { [weak self] code, err in                self?.hideLoading()                self?.showToast(err)            }

3.创建给可访问用户后可在该用户的文件列表内展示。

2.5.2 加密文件夹—解密过程#

1.文件列表数据Model中属性is_encrypt 为 1 时,则为加密文件或文件夹

2.加密文件夹样式的区分。

3.加密文件夹进入的逻辑如下

  • tableview点击相应代理方法,具体参考MyFileViewController.swift
                if file.is_encrypt == 1 {//加密文件                //存储的密码对象                let key = AreaManager.shared.currentArea.scope_token + file.path                    let pwdJsonStr:String = UserDefaults.standard.value(forKey: key) as? String ?? ""                let pwdModel = PasswordModel.deserialize(from: pwdJsonStr)                if pwdModel != nil {                    //计算时间差                    let timeTemp = TimeTool.TimeInterval(FromTime: pwdModel!.saveTime)                    if timeTemp > 72 {//大于72小时                        //重新输入密码                        pushToFolder(isNeedPwd: true, file: file)                    }else{                        //无需输入密码                        pushToFolder(isNeedPwd: false, file: file)                    }                }else{                    //需要重新输入密码                    pushToFolder(isNeedPwd: true, file: file)                }            }else{                //无需输入密码                pushToFolder(isNeedPwd: false, file: file)            }
    private func pushToFolder(isNeedPwd:Bool,file:FileModel){        let vc = FolderViewController()        vc.currentPath = file.path        vc.currentPaths = ["文件",file.name]        vc.encrytRootFile = file        vc.isWriteRoot = (file.write == 1)                let key = AreaManager.shared.currentArea.scope_token + file.path        if file.is_encrypt == 1 {            vc.rootPasswordKey = key        }else{            vc.rootPasswordKey = ""        }        if isNeedPwd {            //存储文件夹根目录Key            self.tipsTestFieldAlert = TipsTestFieldAlertView.show(message: "请输入密码", sureCallback: { pwd in                print("密码是\(pwd)")                NetworkManager.shared.decryptFolder(name: file.path, password: pwd) {[weak self] response in                    guard let self = self else {return}                    //存储时间和密码                    let pwdModel = PasswordModel()                    pwdModel.password = pwd                    //当前时间                    let dateFormatter = DateFormatter()                    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"                    pwdModel.saveTime = dateFormatter.string(from: Date())                    UserDefaults.standard.setValue(pwdModel.toJSONString(prettyPrint:true), forKey: key)                                        //解密成功,进入文件夹                    let nav = BaseNavigationViewController(rootViewController: vc)                    nav.modalPresentationStyle = .fullScreen                    nav.transitioningDelegate = self.transitionUtil                    self.navigationController?.present(nav, animated: true, completion: nil)                } failureCallback: { code, err in                    self.showToast(err)                }            })        }else{            //无需输入密码            let nav = BaseNavigationViewController(rootViewController: vc)            nav.modalPresentationStyle = .fullScreen            nav.transitioningDelegate = transitionUtil            self.navigationController?.present(nav, animated: true, completion: nil)        }    }
  • 加密文件夹进入子集目录需携带最初加密的文件,方便获取路径以及密码
  • 解密过程逻辑如下:(其中规则根据自身项目的需求来定,本项目以超出72小时则自动失效为准则,所以需存储首次解密的时间)
            //存储文件夹根目录Key            self.tipsTestFieldAlert = TipsTestFieldAlertView.show(message: "请输入密码", sureCallback: { pwd in                print("密码是\(pwd)")                NetworkManager.shared.decryptFolder(name: file.path, password: pwd) {[weak self] response in                    guard let self = self else {return}                    //存储时间和密码                    let pwdModel = PasswordModel()                    pwdModel.password = pwd                    //当前时间                    let dateFormatter = DateFormatter()                    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"                    pwdModel.saveTime = dateFormatter.string(from: Date())                    UserDefaults.standard.setValue(pwdModel.toJSONString(prettyPrint:true), forKey: key)                                        //解密成功,进入文件夹                    let nav = BaseNavigationViewController(rootViewController: vc)                    nav.modalPresentationStyle = .fullScreen                    nav.transitioningDelegate = self.transitionUtil                    self.navigationController?.present(nav, animated: true, completion: nil)                } failureCallback: { code, err in                    self.showToast(err)                }            })

2.6 网络架构及临时通道#

2.6.1 网络架构的设计及使用的工具:Alamofire && Moya#

文件路径:Network/ApiService.swift

  • ApiService 请求携带参数
enum ApiService {    /// 创建目录    case createDirectory(area: Area, path: String, name: String, pwd: String = "")    /// 删除文件/目录    case deleteFile(area: Area, paths: [String])    /// 文件/目录重命名    case renameFile(area: Area, path: String, name: String)    /// 获取文件已上传的分块信息    case fileChunks(area: Area, hash: String)    /// 目录下的文件/子目录列表    case fileList(area: Area, path: String, type:Int, page: Int = 1, page_size: Int = 30, pwd: String = "")        //获取数据通道    case temporaryIP(area: Area, scheme: String = "http")}
  • ApiService 请求IP地址(BaseUrl)
extension ApiService: TargetType {    var baseURL: URL {        switch self {        case .deleteFile(let area, _),             .renameFile(let area, _, _),             .fileChunks(let area, _),             .fileList(let area, _, _, _, _, _),                        return URL(string: "\(area.requestURL)")!
        //临时通道接口需向云端服务器请求数据        case .temporaryIP:            return URL(string:"云端服务器地址")!        }    }}
  • ApiService 请求地址的拼接(Path)
    var path: String {
        switch self {        case .deleteFile:            return "/plugin/wangpan/resources"           case .renameFile(_, let path, _):            return "/plugin/wangpan/resources/\(path)"            case .fileChunks(_, let hash):            return "/plugin/wangpan/chunks/\(hash)"         case .fileList(_, let path, _, _, _, _):            return "/plugin/wangpan/resources/\(path)"          case .temporaryIP:            return "/datatunnel"        }    }
  • ApiService 请求方式(Get,Post)
var method: Moya.Method {        switch self {        case .deleteFile:            return .delete         case .renameFile:            return .put          case .fileChunks:            return .get        case .fileList:            return .get        case .temporaryIP:            return .get           }}
  • ApiService 创建Parameters Task
var task: Task {        switch self {         case .deleteFile(_, let paths):              return .requestParameters(parameters: ["paths" : paths], encoding: JSONEncoding.default)        case .renameFile(_, _, let name):            return .requestParameters(parameters: ["name": name], encoding: JSONEncoding.default)        case .fileChunks:            return .requestPlain        case .fileList(_, _, let type, let page ,let page_size, _):            return .requestParameters(parameters: ["type": type,                                                   "page": page,                                                   "page_size": page_size], encoding: URLEncoding.default)                    case .temporaryIP(_, let scheme):            return .requestParameters(                parameters:                    [                        "scheme": scheme                    ],                encoding: URLEncoding.default            )           }}

2.6.2 网络请求的构建#

扩展 Moya数据请求方式

  • 网络请求方法 extension MoyaProvider { ... }
/// 进行网络请求    /// - Parameters:    ///   - target: 请求的target    ///   - modelType: response解析的model    ///   - successCallback: 成功回调    ///   - failureCallback: 失败回调    /// - Returns: 请求    @discardableResult    func requestNetwork<T: BaseModel>(_ target: Target, modelType: T.Type, successCallback: ((_ response: T) -> Void)?, failureCallback: ((_ code: Int, _ errorMessage: String) -> Void)? = nil) -> Moya.Cancellable? {        return request(target) { (result) in            switch result {            case .success(let response):                if printDebugInfo {                    print("-----------------------------< ApiService >--------------------------------")                    print(Date())                    print("---------------------------------------------------------------------------")                    print("header: \(target.headers ?? [:])")                    print("---------------------------------------------------------------------------")                    print("method: \(target.method.rawValue)")                    print("---------------------------------------------------------------------------")                    print("baseUrl: \(target.baseURL)")                    print("---------------------------------------------------------------------------")                    print("target: \(target.path)")                    print("---------------------------------------------------------------------------")                    print("parameters: \(target.task)")                   }                                guard response.statusCode == 200,                 let model = response.data.map(ApiServiceResponseModel<T>.self) else {                    failureCallback?(response.statusCode, "error: \(String(data: response.data, encoding: .utf8) ?? "unknown") code: \(response.statusCode)")                    print("---------------------------------------------------------------------------")                    print("error: \(String(data: response.data, encoding: .utf8) ?? "unknown")")                    print("---------------------------------------------------------------------------\n\n")                    return                }                if model.status == 0 {                    successCallback?(model.data)                } else {                    failureCallback?(model.status, model.reason)                }            case .failure(let error):                let moyaError = error as MoyaError                let statusCode = moyaError.response?.statusCode ?? -1                let errorMessage = "error"                failureCallback?(statusCode, errorMessage)                return            }                    }    }
  • 外部请求数据接口方法: 参考文件路径:Network/NetworkManager.swift
    /// 删除文件或目录    /// - Parameters:    ///   - paths: 要删除的文件或目录 path数组    ///   - successCallback: 成功回调    ///   - failureCallback: 失败回调    /// - Returns: moya网络请求    func deleteFile(area: Area = AreaManager.shared.currentArea, paths: [String], successCallback: ((ExampleResponse) -> Void)?, failureCallback: ((Int, String) -> Void)?) {        requestTemporaryIP(area: area) { [weak self] ip in            guard let self = self else { return }            //获取临时通道地址            if ip != "" {                area.temporaryIP = "http://" + ip + "/api"            }            //请求结果            self.apiService.requestNetwork(.deleteFile(area : area, paths: paths),                                           modelType: ExampleResponse.self,                                           successCallback: successCallback,                                           failureCallback: failureCallback)        } failureCallback: { code, err in            failureCallback?(code,err)        }    }
                NetworkManager.shared.deleteFile(paths: paths) { response in                    //删除文件成功的回调                } failureCallback: { code, err in                    //删除文件失败的回调                }

2.6.3 临时通道含义及功能#

  • 在非局域网的情况下,访问服务器需要走临时通道地址。
  • 获取临时通道的方式如下:
    //获取临时通道地址    private func requestTemporaryIP(area: Area = AreaManager.shared.currentArea, complete:((String)->())?, failureCallback: ((Int, String) -> ())?) {        //在局域网内则直接连接局域网        if area.bssid == NetworkStateManager.shared.getWifiBSSID() && area.bssid != nil || !UserManager.shared.isCloudUser {//局域网            complete?("")            GoFileNewManager.shared.updateHost(host: "\(AreaManager.shared.currentArea.sa_lan_address ?? "")/api")            return        }           //不在SA局域网内且不是云端授权的情况,提示用户云端授权才可在外网使用        if area.bssid != NetworkStateManager.shared.getWifiBSSID() && area.bssid != nil && !UserManager.shared.isCloudUser {            failureCallback?(-1, "智慧中心连接失败,请在智汀家庭云登录后重新授权连接或在局域网内连接")            return        }        //获取本地存储的临时通道地址        let key = area.sa_user_token        let temporaryJsonStr:String = UserDefaults.standard.value(forKey: key) as? String ?? ""        let temporary = TemporaryResponse.deserialize(from: temporaryJsonStr)        //验证是否过期,直接返回IP地址        if let temporary = temporary {//有存储信息            if timeInterval(fromTime: temporary.saveTime , second: temporary.expires_time) {                //地址并未过期                GoFileNewManager.shared.updateHost(host: temporary.host)                complete?(temporary.host)                return            }        }          //过期,请求服务器获取临时通道地址        apiService.requestNetwork(.temporaryIP(area: AreaManager.shared.currentArea, scheme: "http"), modelType: TemporaryResponse.self) { response in            //获取临时通道地址及有效时间,存储在本地            //更新时间和密码            let temporaryModel = TemporaryResponse()            temporaryModel.host = response.host            temporaryModel.expires_time = response.expires_time            //当前时间            let dateFormatter = DateFormatter()            dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"            temporaryModel.saveTime = dateFormatter.string(from: Date())            UserDefaults.standard.setValue(temporaryModel.toJSONString(prettyPrint:true), forKey: key)                        //返回ip地址            complete?(response.host)            GoFileNewManager.shared.updateHost(host: response.host)
        } failureCallback: { code, error in            failureCallback?(code,error)        }    }
  • 临时通道根据下发的有效时间来校验,具体根据自身项目规则来定。
  • 临时通道获取成功后,可以看到需替换掉Moyal请求的BaseUrl。
        requestTemporaryIP(area: area) { [weak self] ip in            guard let self = self else { return }            //获取临时通道地址            if ip != "" {                //替换当前的临时通道地址                area.temporaryIP = "http://" + ip + "/api"            }                //请求结果            self.apiService.requestNetwork(.deleteFile(area : area, paths: paths),                                           modelType: ExampleResponse.self,                                           successCallback: successCallback,                                           failureCallback: failureCallback)        } failureCallback: { code, err in                //获取临时通道失败,反馈出去结果            failureCallback?(code,err)        }

文件路径:Base/Model/Area.swift

    var temporaryIP = "https://sc.zhitingtech.com/api"    /// 请求的地址url(判断请求sa还是sc)    var requestURL: URL {        if bssid == NetworkStateManager.shared.getWifiBSSID()            && bssid != nil {//局域网            return URL(string: "\(sa_lan_address ?? "")/api")!        } else {            return URL(string: temporaryIP)!        }    }

2.7 存储池及存储池分区#

2.7.1 存储池#

在我的页面,若该用户为拥有者,则可出现存储池入口,拥有者可操作存储池的权限。

文件路径:ThirdParty/StorageManage/StorageManageViewController.swift

  • 列表数据根据服务器获取,顶部数据为SA的外接硬盘数据,下部分则为云端创建的存储池数据。
    private func reloadData() {        let semaphore = DispatchSemaphore(value: 1)        if hardDisks.count == 0 && storagePools.count == 0 {            showLoading(.custom(.white_ffffff))        }                DispatchQueue.global().async {            semaphore.wait()                  /// 获取限制硬盘列表            NetworkManager.shared.hardDiskList { [weak self] response in                guard let self = self else { return }                self.hardDisks = response.list                semaphore.signal()
            } failureCallback: { code, err in                semaphore.signal()            }                    /// 挂起任务,等待硬盘列表结果            semaphore.wait()                        /// 获取存储池列表            /// 传0直接获取全部数据 不分页            NetworkManager.shared.storagePoolList(page: 0, pageSize: 0) { [weak self] response in                guard let self = self else { return }                self.storagePools = response.list                semaphore.signal()                            } failureCallback: { code, err in                semaphore.signal()                            }            /// 挂起任务,等待存储池列表结果            semaphore.wait()                        DispatchQueue.main.async {                self.collectionView.mj_header?.endRefreshing()                self.collectionView.reloadData()                self.hideLoading()                semaphore.signal()            }        }    }
2.7.1.1 添加存储池#

文件路径:ThirdParty/StorageManage/AddToStoragePoolViewController.swift

    
/// 添加到新存储池    private func tapAddStorage() {        print("点击添加到新的存储池")        newStoragePoolAlert = SetNameAlertView(setNameType: .createStoragePool, currentName: "")        newStoragePoolAlert?.setNameCallback = { [weak self] name in            guard let self = self else { return }                        if name.isEmpty {                SceneDelegate.shared.window?.makeToast("请输入名称".localizedString)                return            }                        if self.storagePools.map(\.name).contains(name) {                SceneDelegate.shared.window?.makeToast("存储名称不能重复".localizedString)                return            }            /// 请求添加存储池接口            LoadingView.show()            NetworkManager.shared.addStoragePool(name: name, disk_name: self.disk_name) { [weak self] _ in                guard let self = self else { return }                LoadingView.hide()                SceneDelegate.shared.window?.makeToast("添加成功")                self.newStoragePoolAlert?.removeFromSuperview()                self.navigationController?.popViewController(animated: true)            } failureCallback: { [weak self] code, err in                guard let self = self else { return }                LoadingView.hide()                                if code == 205 { // 磁盘挂载失败                    self.newStoragePoolAlert?.removeFromSuperview()                    let singleTipsAlert = SingleTipsAlertView(detail: "硬盘(\(self.disk_name))添加到存储池(\(name))失败,请重新添加。", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定")                    singleTipsAlert.sureCallback = { [weak self] in                        guard let self = self else { return }                        singleTipsAlert.removeFromSuperview()                        self.navigationController?.popViewController(animated: true)                    }                    SceneDelegate.shared.window?.addSubview(singleTipsAlert)                } else {                    SceneDelegate.shared.window?.makeToast(err)                }            }        }        SceneDelegate.shared.window?.addSubview(newStoragePoolAlert!)    }
2.7.1.2 删除存储池#
    private func deletePool(){        let tipsAlert = TipsAlertView(title: "删除确认", detail: "确认删除该存储池吗?删除需要一些时间处理,且删除后,该存储池下的所有分区及其文件夹/文件都全部删除", warning: "操作不可撤销,请谨慎操作!", sureBtnTitle: "确定删除")        tipsAlert.sureCallback = { [weak self] in            guard let self = self else { return }
            tipsAlert.sureBtn.buttonState = .waiting            NetworkManager.shared.deleteStoragePool(name: self.currentStoragePoolName) {[weak self] response in                guard let self = self else { return }                tipsAlert.removeFromSuperview()                //弹框提示后台处理                let singleTipsAlert = SingleTipsAlertView(detail: "正在删除存储池,已为您后台运行,可返回列表刷新查看。", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定")                singleTipsAlert.sureCallback = { [weak self] in                    guard let self = self else { return }                    singleTipsAlert.removeFromSuperview()                    self.navigationController?.popViewController(animated: true)                }                SceneDelegate.shared.window?.addSubview(singleTipsAlert)
//                self.showToast("删除成功")//                self.navigationController?.popViewController(animated: true)            } failureCallback: { [weak self] code, err in                guard let self = self else { return }                self.showToast(err)                tipsAlert.sureBtn.buttonState = .normal            }
        }        SceneDelegate.shared.window?.addSubview(tipsAlert)    }

2.7.2 存储池分区#

2.7.2.1 添加存储池分区#
            NetworkManager.shared.addPartition(name: nameTextFiled.text ?? "", capacity: Float(capacity) ?? 0, unit: capacityButton.titleLabel?.text ?? "GB", pool_name: currentStoragePoolName) {[weak self] response in                LoadingView.hide()                let singleTipsAlert = SingleTipsAlertView(detail: "正在保存分区信息,预计需要一些时间处理,已为您后台运行,可返回列表刷新查看", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定")                singleTipsAlert.sureCallback = { [weak self] in                    guard let self = self else { return }                    singleTipsAlert.removeFromSuperview()                    self.navigationController?.popViewController(animated: true)                }                SceneDelegate.shared.window?.addSubview(singleTipsAlert)                            } failureCallback: {[weak self] code, err in                LoadingView.hide()                self?.showToast(err)            }
2.7.2.2 删除存储池分区#
                    NetworkManager.shared.deletePartition(name: self.currentModel.name, pool_name: self.currentStoragePoolName) {[weak self] response in                        guard let self = self else {return}                        tipsAlert.sureBtn.buttonState = .normal                        tipsAlert.removeFromSuperview()                        //弹框提示后台处理                        let singleTipsAlert = SingleTipsAlertView(detail: "正在删除分区,已为您后台运行,可返回 列表刷新查看。", detailColor: .custom(.black_3f4663), sureBtnTitle: "确定")                        singleTipsAlert.sureCallback = { [weak self] in                            guard let self = self else { return }                            singleTipsAlert.removeFromSuperview()                            self.navigationController?.popViewController(animated: true)                        }                        SceneDelegate.shared.window?.addSubview(singleTipsAlert)                    } failureCallback: {[weak self] code, err in                        guard let self = self else {return}                        tipsAlert.sureBtn.buttonState = .normal                        self.showToast(err)                    }
2.7.2.3 编辑存储池分区#
           judgeInfoChange(editName: nameTextFiled.text ?? "", editCapacity: capacity, unit: capacityButton.titleLabel?.text ?? "GB") {[weak self]                capacityChanged, nameChanged, allowCapacity in                                 guard let self = self else {return}                   if allowCapacity == 2 {                    LoadingView.hide()                    self.showToast("分区内存不能减少")                    return                }                if capacityChanged == 0 && nameChanged == 0{                    LoadingView.hide()                    self.showToast("未修改任何内容")                    return                }                //保存编辑内容                NetworkManager.shared.editPartition(name: self.currentModel.name, new_name: self.nameTextFiled.text ?? "", pool_name: self.currentStoragePoolName, capacity: Float(capacity) ?? 0, unit: self.capacityButton.titleLabel?.text ?? "GB") {[weak self] response in                    LoadingView.hide()                    var showText = ""                    if nameChanged == 1 && capacityChanged == 0 {//仅编辑名称                        showText = "保存成功"                    }else{                        showText = "正在保存分区信息,需要一些时间处理,已为您后台运行,可返回列表刷新查看。"                    }                    let singleTipsAlert = SingleTipsAlertView(detail: showText, detailColor: .custom(.black_3f4663), sureBtnTitle: "确定")                    singleTipsAlert.sureCallback = { [weak self] in                        guard let self = self else { return }                        singleTipsAlert.removeFromSuperview()                        self.navigationController?.popViewController(animated: true)                    }                    SceneDelegate.shared.window?.addSubview(singleTipsAlert)                } failureCallback: {[weak self] code, err in                    LoadingView.hide()                    self?.showToast(err)                }            }

2.8 文件夹管理#

2.8.1 文件夹列表: FolderManageViewController.swift#

    /// 请求数据    private func requestData() {        if folders.count == 0 {            showLoading()        }                let page = (folders.count / 30) + 1        NetworkManager.shared.folderList(page: page, pageSize: 30) { [weak self] response in            guard let self = self else { return }            self.hideLoading()            self.folders.append(contentsOf: response.list)            self.collectionView.mj_header?.endRefreshing()            self.collectionView.mj_footer?.endRefreshing()            if !response.pager.has_more {                self.collectionView.mj_footer?.endRefreshingWithNoMoreData()            }            self.collectionView.reloadData()        } failureCallback: { [weak self] code, err in            self?.collectionView.mj_header?.endRefreshing()            self?.collectionView.mj_footer?.endRefreshing()            self?.collectionView.reloadData()            self?.hideLoading()            self?.showToast(err)        }    }    /// cell配置    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FolderManageCell.reusableIdentifier, for: indexPath) as! FolderManageCell                let folder = folders[indexPath.row]        cell.folder = folder        /// menu按钮回调        cell.menuCallback = { [weak self] in            guard let self = self, let cell = self.collectionView.cellForItem(at: indexPath) else { return }            let x = 35.ztScaleValue            let y = 50.ztScaleValue + Screen.k_nav_height            let alertPoint = cell.convert(CGPoint(x: x, y: y), to: self.view)            let alert = MenuAlert(items: [.init(title: "更改密码", icon: .assets(.icon_lock))], alertPoint: alertPoint)            alert.selectCallback = { [weak self] item in                guard let self = self else { return }                if item.title == "更改密码" {                    let alert = FolderEditPwdAlert()                    alert.saveCallback = { [weak self] old, new, confrim in                        guard let self = self else { return }                        LoadingView.show()                        NetworkManager.shared.editFolderPwd(id: folder.id, oldPwd: old, newPwd: new, confirmPwd: confrim) { [weak self] _ in                            guard let self = self else { return }                            alert.removeFromSuperview()                            LoadingView.hide()                            SceneDelegate.shared.window?.makeToast("修改成功".localizedString)                            self.reloadData()                        } failureCallback: { code, err in                            LoadingView.hide()                            SceneDelegate.shared.window?.makeToast(err)                        }                    }                    SceneDelegate.shared.window?.addSubview(alert)                }            }            SceneDelegate.shared.window?.addSubview(alert)        }                 /// 状态cover 按钮回调        cell.statusCoverCallback = { [weak self] index in            guard let self = self else { return }            switch folder.statusEnum {            case .failToDelete:                if index == 0 { // 修改文件夹失败 - 重试                    self.showLoading()                    NetworkManager.shared.restartAsyncTask(task_id: folder.task_id) { [weak self] _ in                        guard let self = self else { return }                        self.hideLoading()                        self.reloadData()                                            } failureCallback: { [weak self] code, err in                        self?.hideLoading()                        self?.showToast(err)                    }                }            case .failToEdit:                if index == 0 { // 修改文件夹失败 - 确定                    self.showLoading()                    NetworkManager.shared.deleteAsyncTask(task_id: folder.task_id) { [weak self] _ in                        guard let self = self else { return }                        self.hideLoading()                        self.reloadData()                                            } failureCallback: { [weak self] code, err in                        self?.hideLoading()                        self?.showToast(err)                    }                }            default:                break            }        }        return cell    }

2.8.2 文件夹设置: FolderManageSettingViewController.swift#

    /// 保存文件夹设置    @objc    private func tapSave() {        guard let pool = selectedPool, let partition = selectedPartition else { return }
        showLoading()                NetworkManager.shared.setFolderSettings(poolName: pool.name, partitionName: partition.name, autoDel: autoDeleteCell.switchBtn.isOn) { [weak self] _ in            self?.hideLoading()            SceneDelegate.shared.window?.makeToast("保存成功".localizedString)            self?.navigationController?.popViewController(animated: true)        } failureCallback: { [weak self] code, err in            self?.hideLoading()            self?.showToast(err)        }    }        /// 获取文件夹设置    private func getFolderSettings() {        showLoading(.custom(.white_ffffff))        let sp = DispatchSemaphore(value: 1)        DispatchQueue.global().async {            sp.wait()            /// 1获取存储池列表            NetworkManager.shared.storagePoolList(page: 0, pageSize: 0) { [weak self] response in                guard let self = self else { return }                self.storagePools = response.list                sp.signal()
            } failureCallback: { _, _ in                sp.signal()            }            sp.wait()            NetworkManager.shared.getFolderSettings { [weak self] settings in                sp.signal()                guard let self = self else { return }                self.hideLoading()                self.autoDeleteCell.switchBtn.setIsOn(settings.is_auto_del)                 self.storageDefaultCell.nameLabel.text = "\(settings.pool_name)-\(settings.partition_name)"                self.selectedPool = self.storagePools.first(where: { $0.name == settings.pool_name })                if let selectedPool = self.selectedPool {                    self.selectedPartition = selectedPool.lv.first(where: { $0.name == settings.partition_name })                }                            } failureCallback: { [weak self] code, err in                sp.signal()                self?.hideLoading()                self?.showToast(err)            }        }    }

2.8.3 创建、编辑、删除文件夹:EditFolderViewController.swift#

    
    /// 删除文件夹    @objc    private func tapDelete() {        let alert = TipsAlertView(title: "确定删除该文件夹吗?", detail: "删除后,该文件夹及其包含的所有文件夹/文件都全部删除。", warning: "操作不可撤销,请谨慎操作!", sureBtnTitle: "确认删除")                alert.sureCallback = { [weak self] in            guard let self = self else { return }            guard let id = self.type.folderId else { return }
            LoadingView.show()            NetworkManager.shared.deleteFolder(id: id) { [weak self] _ in                LoadingView.hide()                self?.showToast("删除成功".localizedString)                alert.removeFromSuperview()                self?.navigationController?.popViewController(animated: true)            } failureCallback: { [weak self] code, err in                LoadingView.hide()                self?.showToast(err)                alert.removeFromSuperview()            }        }        SceneDelegate.shared.window?.addSubview(alert)    }        /// 保存修改    @objc    private func save() {        if type == .create {            let name = nameCell.textField.text ?? ""            let pool_name = selectedPool?.name ?? ""            let partition_name = selectedPartition?.name ?? ""            let is_encrypt = isSecureCell.selectView.selectedIndex == 0 ? 1 : 0            let pwd = pwdCell1.textField.text ?? ""            let confirmPwd = pwdCell2.textField.text ?? ""            var mode: FolderModel.FolderMode = .shared            if folderTypeCell.selectView.selectedIndex == 0 {                mode = .private            }            showLoading()            NetworkManager.shared.createFolder(name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: is_encrypt, pwd: pwd, confirm_pwd: confirmPwd, mode: mode, auth: members) { [weak self] _ in                self?.showToast("保存成功".localizedString)                self?.navigationController?.popViewController(animated: true)
            } failureCallback: { [weak self] code, err in                self?.hideLoading()                self?.showToast(err)            }        } else {            guard let folder = folder else { return }            let name = nameCell.textField.text ?? ""            let pool_name = selectedPool?.name ?? ""            let partition_name = selectedPartition?.name ?? ""            var mode: FolderModel.FolderMode = .shared            if folderTypeCell.selectView.selectedIndex == 0 {                mode = .private            }            //如果修改了分区            if pool_name != folder.pool_name || partition_name != folder.partition_name {                let tipsAlert = TipsAlertView(title: "存储分区转移".localizedString, titleColor: .custom(.black_3f4663), detail: "\(name)存储分区从“\(folder.pool_name ?? "")-\(folder.partition_name ?? "")”改为“\(pool_name)-\(partition_name)”", detailColor: .custom(.black_3f4663), warning: "修改预计需要一段时间处理,且中途不可取消。确定要修改吗?".localizedString, warningColor: .custom(.red_fe0000), sureBtnTitle: "确定".localizedString)                tipsAlert.sureCallback = { [weak self] in                    guard let self = self else { return }                    self.editFolder(id: folder.id, name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: folder.is_encrypt, mode: mode, auth: self.members)                    tipsAlert.removeFromSuperview()                }                                SceneDelegate.shared.window?.addSubview(tipsAlert)                            } else {                editFolder(id: folder.id, name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: folder.is_encrypt, mode: mode, auth: members)            }            }    }        /// 编辑文件夹    private func editFolder(id: Int, name: String, pool_name: String, partition_name: String, is_encrypt: Int, mode: FolderModel.FolderMode, auth: [User]) {        showLoading()        NetworkManager.shared.editFolder(id: id, name: name, pool_name: pool_name, partition_name: partition_name, is_encrypt: is_encrypt, mode: mode, auth: auth) { [weak self] _ in            guard let self = self else { return }            if pool_name != self.folder?.pool_name || partition_name != self.folder?.partition_name {                //如果修改了分区                let alert = SingleTipsAlertView(detail: "存储分区转移" + "\n\n" + "\(name)存储分区正在从“\(self.folder?.pool_name ?? "")-\(self.folder?.partition_name ?? "")”改为“\(pool_name)-\(partition_name)”,已为您后台运行,可返回列表查看。", sureBtnTitle: "确定".localizedString)                alert.sureCallback = { [weak self] in                    guard let self = self else { return }                    alert.removeFromSuperview()                    self.showToast("保存成功".localizedString)                    self.navigationController?.popViewController(animated: true)                }                SceneDelegate.shared.window?.addSubview(alert)
            } else {                self.showToast("保存成功".localizedString)                self.navigationController?.popViewController(animated: true)            }        } failureCallback: { [weak self] code, err in            guard let self = self else { return }            self.hideLoading()            if code == 20019 {                //目标分区容量不足,不能迁移                let errAlert = SingleTipsAlertView(detail: "存储分区修改失败".localizedString + "\n\n" + "分区容量不足!".localizedString, sureBtnTitle: "确定".localizedString)                errAlert.sureCallback = {                    errAlert.removeFromSuperview()                }                SceneDelegate.shared.window?.addSubview(errAlert)                            } else {                self.showToast(err)            }         }    }