WiFi文件上传框架SGWiFiUpload

作者: Soulghost | 来源:发表于2016-06-30 13:54 被阅读622次

    背景

    在iOS端由于文件系统的封闭性,文件的上传变得十分麻烦,一个比较好的解决方案是通过局域网WiFi来传输文件并存储到沙盒中。

    简介

    SGWiFiUpload是一个基于CocoaHTTPServer的WiFi上传框架。CocoaHTTPServer是一个可运行于iOS和OS X上的轻量级服务端框架,可以处理GET和POST请求,通过对代码的初步改造,实现了iOS端的WiFi文件上传与上传状态监听。

    下载与使用

    目前已经做成了易用的框架,上传到了GitHub,点击这里进入,欢迎Star!

    请求的处理

    CocoaHTTPServer通过HTTPConnection这一接口实现类来回调网络请求的各个状态,包括对请求头、响应体的解析等。为了实现文件上传,需要自定义一个继承HTTPConnection的类,这里命名为SGHTTPConnection,与文件上传有关的几个方法如下。

    解析文件上传的请求头

    - (void)processStartOfPartWithHeader:(MultipartMessageHeader*) header {
        
        // in this sample, we are not interested in parts, other then file parts.
        // check content disposition to find out filename
    
        MultipartMessageHeaderField* disposition = [header.fields objectForKey:@"Content-Disposition"];
        NSString* filename = [[disposition.params objectForKey:@"filename"] lastPathComponent];
    
        if ( (nil == filename) || [filename isEqualToString: @""] ) {
            // it's either not a file part, or
            // an empty form sent. we won't handle it.
            return;
        }
        // 这里用于发出文件开始上传的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SGFileUploadDidStartNotification object:@{@"fileName" : filename ?: @"File"}];
        });
        // 这里用于设置文件的保存路径,先预存一个空文件,然后进行追加写内容
        NSString *uploadDirPath = [SGWiFiUploadManager sharedManager].savePath;
        BOOL isDir = YES;
        if (![[NSFileManager defaultManager]fileExistsAtPath:uploadDirPath isDirectory:&isDir ]) {
            [[NSFileManager defaultManager]createDirectoryAtPath:uploadDirPath withIntermediateDirectories:YES attributes:nil error:nil];
        }
        
        NSString* filePath = [uploadDirPath stringByAppendingPathComponent: filename];
        if( [[NSFileManager defaultManager] fileExistsAtPath:filePath] ) {
            storeFile = nil;
        }
        else {
            HTTPLogVerbose(@"Saving file to %@", filePath);
            if(![[NSFileManager defaultManager] createDirectoryAtPath:uploadDirPath withIntermediateDirectories:true attributes:nil error:nil]) {
                HTTPLogError(@"Could not create directory at path: %@", filePath);
            }
            if(![[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil]) {
                HTTPLogError(@"Could not create file at path: %@", filePath);
            }
            storeFile = [NSFileHandle fileHandleForWritingAtPath:filePath];
            [uploadedFiles addObject: [NSString stringWithFormat:@"/upload/%@", filename]];
        }
    }
    

    其中有中文注释的两处是比较重要的地方,这里根据请求头发出了文件开始上传的通知,并且往要存放的路径写一个空文件,以便后续追加内容。

    上传过程中的处理

    - (void) processContent:(NSData*) data WithHeader:(MultipartMessageHeader*) header 
    {
        // here we just write the output from parser to the file.
        // 由于除去文件内容外,还有HTML内容和空文件通过此方法处理,因此需要过滤掉HTML和空文件内容
        if (!header.fields[@"Content-Disposition"]) {
            return;
        } else {
            MultipartMessageHeaderField *field = header.fields[@"Content-Disposition"];
            NSString *fileName = field.params[@"filename"];
            if (fileName.length == 0) return;
        }
        self.currentLength += data.length;
        CGFloat progress;
        if (self.contentLength == 0) {
            progress = 1.0f;
        } else {
            progress = (CGFloat)self.currentLength / self.contentLength;
        }
        dispatch_async(dispatch_get_main_queue(), ^{
           [[NSNotificationCenter defaultCenter] postNotificationName:SGFileUploadProgressNotification object:@{@"progress" : @(progress)}]; 
        });
        if (storeFile) {
            [storeFile writeData:data];
        }
    }
    

    这里除了拼接文件内容以外,还发出了上传进度的通知,当前方法中只能拿到这一段文件的长度,总长度需要通过下面的方法拿到。

    获取文件大小

    - (void)prepareForBodyWithSize:(UInt64)contentLength
    {
        HTTPLogTrace();
        // 设置文件总大小,并初始化当前已经传输的文件大小。
        self.contentLength = contentLength;
        self.currentLength = 0;
        // set up mime parser
        NSString* boundary = [request headerField:@"boundary"];
        parser = [[MultipartFormDataParser alloc] initWithBoundary:boundary formEncoding:NSUTF8StringEncoding];
        parser.delegate = self;
    
        uploadedFiles = [[NSMutableArray alloc] init];
    }
    

    处理传输完毕

    - (void) processEndOfPartWithHeader:(MultipartMessageHeader*) header
    {
        // as the file part is over, we close the file.
        // 由于除去文件内容外,还有HTML内容和空文件通过此方法处理,因此需要过滤掉HTML和空文件内容
        if (!header.fields[@"Content-Disposition"]) {
            return;
        } else {
            MultipartMessageHeaderField *field = header.fields[@"Content-Disposition"];
            NSString *fileName = field.params[@"filename"];
            if (fileName.length == 0) return;
        }
        [storeFile closeFile];
        storeFile = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SGFileUploadDidEndNotification object:nil];
        });
    }
    

    这里关闭了文件管道,并且发出了文件上传完毕的通知。

    开启Server

    CocoaHTTPServer默认的Web根目录为MainBundle,他会在目录下寻找index.html,文件上传的请求地址为upload.html,当以POST方式请求upload.html时,请求会被Server拦截,并且交由HTTPConnection处理。

    - (BOOL)startHTTPServerAtPort:(UInt16)port {
        HTTPServer *server = [HTTPServer new];
        server.port = port;
        self.httpServer = server;
        [self.httpServer setDocumentRoot:self.webPath];
        [self.httpServer setConnectionClass:[SGHTTPConnection class]];
        NSError *error = nil;
        [self.httpServer start:&error];
        return error == nil;
    }
    

    在HTML中发送POST请求上传文件

    在CocoaHTTPServer给出的样例中有用于文件上传的index.html,要实现文件上传,只需要一个POST方法的form表单,action为upload.html,每一个文件使用一个input标签,type为file即可,这里为了美观对input标签进行了自定义。
    下面的代码演示了能同时上传3个文件的index.html代码。

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    <html>
        <head>
            <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
        </head>
        <style>
        body {
            margin: 0px;
            padding: 0px;
            font-size: 12px;
            background-color: rgb(244,244,244);
            text-align: center;
        }
        #container {
            margin: auto;
        }
        
        #form {
            margin-top: 60px;
        }
        
        .upload {
            margin-top: 2px;
        }
        
        #submit input {
            background-color: #ea4c88;
            color: #eee;
            font-weight: bold;
            margin-top: 10px;
            text-align: center;
            font-size: 16px;
            border: none;
            width: 120px;
            height: 36px;
        }
        
        #submit input:hover {
            background-color: #d44179;
        }
        
        #submit input:active {
            background-color: #a23351;
        }
        
        .uploadField {
            margin-top: 2px;
            width: 200px;
            height: 22px;
            font-size: 12px;
        }
        
        .uploadButton {
            background-color: #ea4c88;
            color: #eee;
            font-weight: bold;
            text-align: center;
            font-size: 15px;
            border: none;
            width: 80px;
            height: 26px;
        }
        
        .uploadButton:hover {
            background-color: #d44179;
        }
        
        .uploadButton:active {
            background-color: #a23351;
        }
        
        </style>
        <body>
            <div id="container">
                <div id="form">
                    <h2>WiFi File Upload</h2>
                    <form name="form" action="upload.html" method="post" enctype="multipart/form-data" accept-charset="utf-8">
                        <div class="upload">
                            <input type="file" name="upload1" id="upload1" style="display:none" onChange="document.form.path1.value=this.value">
                                <input class="uploadField" name="path1" readonly>
                                    <input class="uploadButton" type="button" value="Open" onclick="document.form.upload1.click()">
                        </div>
                        <div class="upload">
                            <input type="file" name="upload2" id="upload2" style="display:none" onChange="document.form.path2.value=this.value">
                                <input class="uploadField" name="path2" readonly>
                                    <input class="uploadButton" type="button" value="Open" onclick="document.form.upload2.click()">
                        </div>
                        <div class="upload">
                            <input type="file" name="upload3" id="upload3" style="display:none" onChange="document.form.path3.value=this.value">
                                <input class="uploadField" name="path3" readonly>
                                    <input class="uploadButton" type="button" value="Open" onclick="document.form.upload3.click()">
                                        </div>
                        <div id="submit"><input type="submit" value="Submit"></div>
                    </form>
                </div>
            </div>
        </body>
    </html>
    

    表单提交后,会进入upload.html页面,该页面用于说明上传完毕,下面的代码实现了3秒后的重定向返回。

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
    <html>
        <head>
            <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">
            <meta http-equiv=refresh content="3;url=index.html">
        </head>
        <body>
            <h3>Upload Succeeded!</h3>
            <p>The Page will be back in 3 seconds</p>
        </body>
    </html>
    

    相关文章

      网友评论

      • 牛蛋:楼主 按照您说的 IP+端口 在浏览器中输入 一直加载不出来
      • 9ef5bbd7270f:楼主可否讲解一下电脑浏览和下载iOS端的文件是如何实现的
        因为太有钱:@Soulghost 我现在需要做的是 把手机相册的也上传 总不能把所有图片写进沙河吧 那样就太浪费了 这个有什么思路吗
        Soulghost:@CoderMaurice 一般方案就是在客户端开一个Server,然后电脑去请求。不过这样最多只能暴露当前app沙盒的内容。
      • 攻城狮小强:我直接运行demo,手机和电脑连接的同一无线网,访问IP+Port,电脑一直都是无法打开页面,楼主知道是啥问题吗?
        攻城狮小强:@牛蛋 我没解决,但一样的代码,我朋友却可以用,也许是网络问题吧。
        牛蛋:我也遇到同样的问题了,不知道你那边怎么解决的?
      • 云逸枫林:项目里用了这个做电脑传文件给app,确实很好用,感谢作者了.

        另外现在项目中还有需要一边传视频文件一边播放的需求, 我找了很多资料,看起来还是没什么头绪, 特来请教一下, 有没有什么思路.
        目前研究的方向是用里面包含的CocoaHTTPServer启动一个http server,然后app上的播放器向这个app上的自建http server发送请求, 想要模拟正常的播放器加载http视频资源的方式. 然后,用这个server访问这个没有传完的视频文件,读取数据返回给播放器..... 不知道这样可行么...
        云逸枫林:我自己实践发现, server里面每次读取这个没有传完的视频文件 都是直接返回了, 没办法做持续加载的那种形式, 就是如果播放器请求的那段数据还没有接收到,就让播放器等待, 一旦接收到就立即返回给播放器
      • 魔鬼分界线:使用简单,到位
        Soulghost:@魔鬼分界线 多谢支持

      本文标题:WiFi文件上传框架SGWiFiUpload

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