美文网首页
简单好用的macOS文件共享——EasyShare

简单好用的macOS文件共享——EasyShare

作者: AntichristM | 来源:发表于2020-05-18 14:42 被阅读0次
    EasyShare logo

    前言

    首先,我是一个Android开发者,这也是我第一次用Swift写东西,所以可能会有并不太地道的用法,请见谅。先看一下软件基本信息:

    1. 开发语言:Swift 5
    2. 操作系统:macOS 10.13及以上
    3. 功能:在同一个网络下,生成文件对应的二维码及链接,提供给其他设备进行下载
    4. 形式:GUI

    看一下演示效果:(压缩得有点狠,将就看一看)


    效果演示

    功能分析

    1. 本地开启http服务
    2. 编写接口将文件写入流
    3. 添加GUI实现

    本地开启http服务

    通过各种搜索工具,我找到了一个叫做Perfect的库。同时,在查看Perfect文档的同时,我也发现Perfect做了Sqlite的ORM,所以也就一起拿来用了。本来是准备做常驻服务来着,所以就做了数据库来存东西,但是后来觉得不太方便使用,又去掉了,现在只是当作缓存在使用。
    首先看一下两个表:tb_config和tb_share.

    tb_config

    key type remark
    key String 主键
    value String 配置参数
    remark String 备注

    tb_share

    key type remark
    id Int 主键
    name String 文件名
    key String 文件标识,用于展示分享链接
    path String 文件路径
    createTime Int64 创建时间

    数据库差不多就这样,看一下tb_share里面key的生成,其实就是时间戳加上文件名取md5之后再base64。能得到一个类似这样的结果:h-rdq6pojZWQcCr0j0kRAg

    //
    //  DataUtils.swift
    //  EasyShare
    //
    //  Created by Michael Lee on 2020/5/4.
    //  Copyright © 2020 Michael Lee. All rights reserved.
    //
    import PerfectCrypto
    
    import Foundation
    
    class DataUtils{
        
        static func generateKey(name :String) -> String{
            let md5 = "\(NSDate().timeIntervalSince1970)\(name)".digest(.md5)?.encode(.base64url)
            return String(validatingUTF8: md5!) ?? "\(NSDate().timeIntervalSince1970)\(name)"
        }
        
    }
    
    

    编写接口将文件写入流

    现在通过Perfect创建本地服务。

    //
    //  Server.swift
    //  EasyShare
    //
    //  Created by Michael Lee on 2020/5/3.
    //  Copyright © 2020 Michael Lee. All rights reserved.
    //
    
    import Foundation
    import PerfectHTTP
    import PerfectLib
    import PerfectHTTPServer
    
    class ShareServer{
    
        static let instance = ShareServer()
        
        init() {
            //添加api路由
            addApi()
            //添加web路由,本来是准备做h5展示再点击下载,后来觉得没必要,就没做
            addWeb()
            routes.add(api)
            routes.add(web)
        }
        
        //标记服务是否开启
        var state = 0
        
        //默认端口号
        var port :UInt16 = 8899
        
        let queue = DispatchQueue.global()
        
        var httpServer = HTTPServer()
        
        var routes = Routes()
        
        var api = Routes(baseUri: "/api")
        
        var web = Routes(baseUri: "/web")
    
        /// 开启服务
        func start(){
            if state == 1 {
                return
            }
            state = 1
            httpServer.serverName = "EasyShare"
            httpServer.addRoutes(routes)
            do{
                try httpServer.serverPort = UInt16(DbHelper.getConfig(key: ConfigMap.port).value) ?? port
            }catch{
                httpServer.serverPort = port
            }
            port = httpServer.serverPort
            
            queue.async {
                do{
                    try self.httpServer.start()
                }catch{
                    self.state = 0
                    fatalError("\(error)")
                }
            }
        }
        
        /// 停止服务
        func stop(){
            httpServer.stop()
            state = 0
        }
        
        func addApi() {
            /// 获取分享详情
            api.add(method: .get, uri: "/info/{key}", handler: {request,response in
                let key = request.urlVariables["key"]
                let share = ShareDTO()
                do {
                    try share.find([("key",key!)])
                }catch{
                    do{
                        try response.setBody(json: self.createResponseBody(code: 500, message: "key error", data: nil))
                    }catch{
                        response.setBody(string: "\(error)")
                    }
                    response.completed()
                    return
                }
                if(share.id == 0){
                    do{
                        try response.setBody(json: self.createResponseBody(code: 404, message: "key not found", data: nil))
                    }catch{
                        response.setBody(string: "\(error)")
                    }
                    response.completed()
                }else{
                    do{
                        try response.setBody(json:
                            self.createResponseBody(
                                code: 200,
                                message: "success",
                                data: share.asDataDict()))
                    }catch{
                        response.setBody(string: "\(error)")
                    }
                    response.completed()
                }
            })
            
            /// 下载
            api.add(method: .get, uri: "/download/{key}", handler: {request,response in
                let share = ShareDTO()
                let key = request.urlVariables["key"]
                do {
                    try share.find([("key",key!)])
                }catch{
                    do{
                        try response.setBody(json: self.createResponseBody(code: 500, message: "key error", data: nil))
                    }catch{
                        response.setBody(string: "\(error)")
                    }
                    response.completed()
                    return
                }
                
                if(share.id == 0){
                    do{
                        try response.setBody(json: self.createResponseBody(code: 404, message: "key not found", data: nil))
                    }catch{
                        response.setBody(string: "\(error)")
                    }
                    response.completed()
                    return
                }
                //获取文件
                let file = File(share.path + "/" + share.name)
                if(file.exists && !file.isDir){
                    do{
                        try file.open()
                        let size = file.size
                        let contentType = MimeType.forExtension(file.path.filePathExtension)
                        response.status = .ok
                        response.isStreaming = true
                        response.setHeader(.contentType, value: contentType)
                        response.setHeader(.contentLength, value: "\(size)")
                        response.setHeader(.acceptRanges, value: "bytes")
                        response.setHeader(.contentDisposition, value: "attachment;filename=\"\(share.name)\"")
                        self.pushBody(response: response, file: file)
                    }catch{
                        response.setBody(string: "\(error)")
                        response.completed()
                    }
                }else{
                    //文件不存在
                    do{
                        try response.setBody(json: self.createResponseBody(code: 404, message: "file not exists", data: nil))
                    }catch{
                        response.setBody(string: "\(error)")
                    }
                    response.completed()
                }
            })
            
        }
        
        /// 往response里面写流
        func pushBody(response:HTTPResponse,file:File){
            let readSize = 5 * 1024 * 1024//每次读5m
            var bytes :[UInt8]
            do {
               bytes = try file.readSomeBytes(count: readSize)
            }catch{
               bytes = [UInt8]()
            }
            if(bytes.count==0){
                file.close()
                response.completed()
                return
            }
            response.appendBody(bytes: bytes)
            response.push(callback: { bool in
                if(bool){
                    self.pushBody(response: response, file: file)
                }else{
                    file.close()
                    response.completed(status: HTTPResponseStatus.gatewayTimeout)
                }
            })
        }
        
        func addWeb() {
        }
        
        /// 生成json返回值
        func createResponseBody(code:Int,message:String,data:Any?) -> [String:Any] {
            return ["code":code,"message":message,"data":data ?? [String:Any]()]
        }
        
    }
    
    

    以上就是本地服务的所有代码,本来东西很少,所以就没有分开了。

    添加GUI实现

    我找了很久,终于找到一个叫做Share Extension的东西,就是会显示在分享菜单里面,但是这个东西怎么用,根本就没有文档,而且网上找出来全部是iOS相关的东西。这里我就自己摸索着写的。

    //
    //  ShareViewController.swift
    //  ShareExtension
    //
    //  Created by Michael Lee on 2020/5/4.
    //  Copyright © 2020 Michael Lee. All rights reserved.
    //
    
    import Cocoa
    import PerfectLib
    import SwiftUI
    
    class ShareViewController: NSViewController {
        
        @IBOutlet weak var titleCell: NSTextFieldCell!
        @IBOutlet weak var imageCell: NSImageCell!
        @IBOutlet weak var urlCell: NSTextFieldCell!
        
        var id = 0
        var url = ""
        
        override var nibName: NSNib.Name? {
            return NSNib.Name("ShareViewController")
        }
    
        override func loadView() {
            super.loadView()
            // 这个可以获取到分享是点的哪里
            let provider = (self.extensionContext!.inputItems[0] as! NSExtensionItem).attachments?[0]
            // 获取URL对象
            provider?.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: {data,error in
                let url = data as! NSURL
                // 获取绝对路径
                let path = url.absoluteString
                if(path!.starts(with: "file://")){
                    //走文件分享的方法
                    self.shareFile(path: url.path!)
                }else if(path!.starts(with: "http://") || path!.starts(with: "https://")){
                    //分享网页链接
                    self.shareWeb(path: path!)
                }else{
                    //都不是,则直接结束
                    self.extensionContext!.completeRequest(returningItems: [NSExtensionItem()], completionHandler: nil)
                }
            })
        }
    
        func shareFile(path:String) {
            // 初始化数据库
            DbHelper.create()
            // 开启本地服务
            ShareServer.instance.start()
            let file = File(path)
            if(file.exists){
                // 开始写入数据库
                let splits = file.path.split(separator: "/")
                let name = splits[splits.count-1]
                let share = ShareDTO()
                share.name = "\(name)"
                share.path = file.path.replacingOccurrences(of: "/\(name)", with: "")
                share.key = DataUtils.generateKey(name: share.name)
                do{
                    try share.save{ id in
                        share.id = id as! Int
                        self.id = share.id
                    }
                }catch{
                    fatalError("\(error)")
                }
                // 这一句只是打印一下数据库中所有的分享,调试用的,没啥实质用处
                self.findAllShare()
                // 显示窗口信息
                self.showWindowInfo(
                    url: "http://\(self.getIFAddresses()[0]):\(ShareServer.instance.port)/api/download/\(share.key)",
                    title: share.name
                )
            }else{
                self.extensionContext!.completeRequest(returningItems: [NSExtensionItem()], completionHandler: nil)
            }
        }
        
        func shareWeb(path:String){
            // 直接显示窗口信息
            self.showWindowInfo(url: path, title: "我是真的不知道Title怎么获取😅👌")
        }
        
        func showWindowInfo(url:String,title:String) {
            self.url = url
            self.urlCell.stringValue = url
            self.titleCell.stringValue = title
            self.imageCell.image = self.generateQRCodeImage(self.url, size: NSSize(width: 600, height: 600))
        }
        
        @IBAction func copy(_ sender: NSButton) {
            // 拷贝到粘贴板
            let pasteboard = NSPasteboard.general
            pasteboard.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
            let b = pasteboard.setString(url, forType: NSPasteboard.PasteboardType.string)
            print(b)
        }
        
        @IBAction func send(_ sender: AnyObject?) {
            // 结束分享
            ShareServer.instance.stop()
            let share = ShareDTO()
            do{
                try share.delete(id)
            }catch{
                NSLog("\(error)")
            }
            self.extensionContext!.completeRequest(returningItems: [NSExtensionItem()], completionHandler: nil)
        }
    
        /// 生成二维码
        func generateQRCodeImage(_ content: String, size: NSSize) -> NSImage?{
            // 创建滤镜
            guard let filter = CIFilter(name: "CIQRCodeGenerator") else {return nil}
            // 还原滤镜的默认属性
            filter.setDefaults()
            //1.3 设置生成的二维码的容错率
            //value = @"L/M/Q/H"
            filter.setValue("L", forKey: "inputCorrectionLevel")
            // 设置需要生成的二维码数据
            let contentData = content.data(using: String.Encoding.utf8)
            filter.setValue(contentData, forKey: "inputMessage")
    
    
            // 从滤镜中取出生成的图片
            guard let ciImage = filter.outputImage else {return nil}
    
            let context = CIContext(options: nil)
            let bitmapImage = context.createCGImage(ciImage, from: ciImage.extent)
    
            let colorSpace = CGColorSpaceCreateDeviceGray()
            let bitmapContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: CGImageAlphaInfo.none.rawValue)
    
            //draw image
            let scale = min(size.width / ciImage.extent.width, size.height / ciImage.extent.height)
            bitmapContext!.interpolationQuality = CGInterpolationQuality.none
            bitmapContext?.scaleBy(x: scale, y: scale)
            bitmapContext?.draw(bitmapImage!, in: ciImage.extent)
    
            //保存bitmap到图片
            guard let scaledImage = bitmapContext?.makeImage() else {return nil}
    
            return NSImage(cgImage: scaledImage, size: size)
        }
        
        /// 获取本地IP地址
        func getIFAddresses() -> [String] {
            var addresses = [String]()
            
            // Get list of all interfaces on the local machine:
            var ifaddr : UnsafeMutablePointer<ifaddrs>? = nil
            if getifaddrs(&ifaddr) == 0 {
              
              var ptr = ifaddr
              while ptr != nil {
                let flags = Int32((ptr?.pointee.ifa_flags)!)
                var addr = ptr?.pointee.ifa_addr.pointee
                
                // Check for running IPv4, IPv6 interfaces. Skip the loopback interface.
                if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) {
                  if addr?.sa_family == UInt8(AF_INET) && addr?.sa_family != UInt8(AF_INET6) {
                    
                    // Convert interface address to a human readable string:
                    var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
                    if (getnameinfo(&addr!, socklen_t((addr?.sa_len)!), &hostname, socklen_t(hostname.count),
                                    nil, socklen_t(0), NI_NUMERICHOST) == 0) {
                      if let address = String(validatingUTF8: hostname) {
                        addresses.append(address)
                      }
                    }
                  }
                }
                ptr = ptr?.pointee.ifa_next
              }
     
                freeifaddrs(ifaddr)
            }
            print("Local IP \(addresses)")
            return addresses
        }
        
        func findAllShare(){
            let share = ShareDTO()
            do{
                try share.findAll()
                let rows = share.rows()
                for row in rows{
                    print(row.asDataDict())
                }
            }catch{
                fatalError("\(error)")
            }
        }
    
    }
    
    

    收个尾

    主要代码都在上面了,其实就两个主要的类。贴个GitHub地址,欢迎各位点星。
    GitHub
    要直接用的也可以在release里面直接下载,有任何意见或建议欢迎提issue。

    相关文章

      网友评论

          本文标题:简单好用的macOS文件共享——EasyShare

          本文链接:https://www.haomeiwen.com/subject/ghvuohtx.html