• MIME TYPE描述 内容类型(Content-Type),这个头部领域用于指定消息的类型。一般以下面的形式出现。[type]/[subtype] type有下面的形式。...Text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者...
    MIME TYPE描述
    

    内容类型(Content-Type),这个头部领域用于指定消息的类型。一般以下面的形式出现。[type]/[subtype]

    type有下面的形式。

    • Text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的;
    • Multipart:用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的数据;
    • Application:用于传输应用程序数据或者二进制数据;
    • Message:用于包装一个E-mail消息;
    • Image:用于传输静态图片数据;
    • Audio:用于传输音频或者音声数据;
    • Video:用于传输动态影像数据,可以是与音频编辑在一起的视频数据格式。

    subtype用于指定type的详细形式。content-type/subtype配对的集合和与此相关的参数,将随着时间而增长。为了确保这些值在一个有序而且公开的状态下开发,MIME使用Internet Assigned Numbers Authority (IANA)作为中心的注册机制来管理这些值。常用的subtype值如下所示:

    • text/plain(纯文本
    • text/html(HTML文档)
    • application/xhtml+xml(XHTML文档)
    • image/gif(GIF图像)
    • image/jpeg(JPEG图像)【PHP中为:image/pjpeg】
    • image/png(PNG图像)【PHP中为:image/x-png】
    • video/mpeg(MPEG动画)
    • application/octet-stream(任意的二进制数据)
    • application/pdf(PDF文档)
    • application/msword(Microsoft Word文件)
    • message/rfc822(RFC 822形式)
    • multipart/alternative(HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示)
    • application/x-www-form-urlencoded(使用HTTP的POST方法提交的表单)
    • multipart/form-data(同上,但主要用于表单提交时伴随文件上传的场合
    展开全文
  • iOS 上传怎样上传txt 文件 求代码演示 我要把收集的东西上传到又拍云
  • ios下使用AFNetworking通过Nginx服务器实现大文件上传

    服务器配置:

    服务器环境为:Nginx(1.3.8) + upload module

    nginx.conf 配置

    server {
        client_max_body_size 100m;
        listen       80;
    
        # Upload form should be submitted to this location
        location /upload {
            # Pass altered request body to this location
            upload_pass   @test;
    
            # Store files to this directory
            # The directory is hashed, subdirectories 0 1 2 3 4 5 6 7 8 9 should exist
            upload_store /tmp 1;
            
            # Allow uploaded files to be read only by user
            upload_store_access user:r;
    
            # Set specified fields in request body
            upload_set_form_field $upload_field_name.name "$upload_file_name";
            upload_set_form_field $upload_field_name.content_type "$upload_content_type";
            upload_set_form_field $upload_field_name.path "$upload_tmp_path";
    
            # Inform backend about hash and size of a file
            upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
            upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
    
            upload_pass_form_field "^submit$|^description$";
    
            upload_cleanup 400 404 499 500-505;
        }
    
        # Pass altered request body to a backend
        location @test {
            proxy_pass   http://localhost:8080;
        }
    }


    客户端实现代码:

        NSURL *url= [NSURL URLWithString:[NSString stringWithFormat:@"%@?json=%@",nginxserverUrl, jsonUrlStr]];


        AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

        manager.requestSerializer = [AFHTTPRequestSerializer serializer];

        manager.responseSerializer = [AFHTTPResponseSerializer serializer];

        manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];

        manager.requestSerializer.timeoutInterval = 60;

        NSURL *filePath = [NSURL fileURLWithPath:@"文件路径"];

        [manager POST:[NSString stringWithFormat:@"%@",url] parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {

            

            [formData appendPartWithFileURL:filePath name:@"attach" error:nil];


        } success:^(AFHTTPRequestOperation *operation, id responseObject) {

            NSString *responseStr = [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding];

            NSLog(@"Success: %@", responseStr);

        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {

            NSLog(@"sendFileToServer -> error = %@", [error userInfo]);

        }];


    更多IOS开发相关知识,请访问http://www.iosask.com


    展开全文
  • 概述 一说到文件上传,想必大家都并不陌生,更何况是利用AFNetworking(PS:...如果利用AF来实现,无非就是客户端调用AF提供的文件上传接口即可,API如下所示: - (nullable NSURLSessionDataTask *)POST:(NSStri...

    概述

    一说到文件上传,想必大家都并不陌生,更何况是利用AFNetworking(PS:后期统称AF)来做,那更是小菜一碟。比如开发中常见的场景:头像上传九宫格图片上传...等等,这些场景无一不使用到文件上传的功能。如果利用AF来实现,无非就是客户端调用AF提供的文件上传接口即可,API如下所示:

    - (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                                 parameters:(nullable id)parameters
                  constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
                                   progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                    success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                    failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
    

    上面这种场景,主要是针对一些小资源文件的上传,上传过程耗时较短,用户可以接受。但是一旦资源文件过大(比如1G以上),则必须要考虑上传过程网络中断的情况。试想我们还是采用上述方案,一口气把这整个1G的资源文件上传到服务器,这显然是不现实的,就算服务器答应,用户也不答应的。考虑到网络使用中断或服务器上传异常...等场景,那么我们恢复网络后又得重新从头开始上传,那之前已经上传完成的部分资源岂不作废,这种耗时耗力的工作,显然是不符合常理的。为了解决大文件上传的存在如此鸡肋的问题,从而诞生了一个叫:分片上传(断点续上传)

    分片上传(断点续上传) 主要是为了保证在网络中断后1G的资源文件已上传的那部分在下次网络连接时不必再重传。所以我们本地在上传的时候,要将大文件进行切割分片,比如分成1024*1024B,即将大文件分成1M的片进行上传,服务器在接收后,再将这些片合并成原始文件,这就是 分片 的基本原理。断点续传要求本地要记录每一片的上传的状态,我通过三个状态进行了标记(waiting loading finish),当网络中断,再次连接后,从断点处进行上传。服务器通过文件名、总片数判断该文件是否已全部上传完成。

    弄懂了分片上传(断点续上传) 的基本原理,其核心就是分片,然后将分割出来的的每一,按照类似上传头像的方式上传到服务器即可,全部上传完后再在服务端将这些小数据片合并成为一个资源。

    分片上传引入了两个概念:块(block)片(fragment)。每个块由一到多个片组成,而一个资源则由一到多个块组成。他们之间的关系可以用下图表述:

    文件资源组成关系.png

     

    本文笔者将着重分析分片上传实现的具体过程以及细节处理,争取把里面的所有涵盖的知识点以及细节处理分析透彻。希望为大家提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。

    效果图如下:

     

    FileUpload.gif

    知识点

    虽然分片上传的原理看似非常简单,但是落实到具体的实现,其中还是具有非常多的细节分析和逻辑处理,而且都是我们开发中不常用到的知识点,这里笔者就总结了一下分片上传所用到的知识点和使用场景,以及借助一些第三方框架,来达到分片上传的目的。

    • 图片和视频资源的获取
      所谓文件上传,前提必须得有文件,而文件一般是本地文件,本地文件的获取来源一般是系统相册获取,关于如何从系统相册中获取图片或视频资源,这里笔者采用TZImagePickerController一个支持多选、选原图和视频的图片选择器,同时有预览、裁剪功能,支持iOS6+第三方框架。根据TZImagePickerControllerDelegate返回的资源(图片、视频)数据,然后利用TZImageMananger提供的API,获取到原始图片和视频资源。关键API如下:具体使用请参照TZImagePickerController提供Demo。

       /// 获取原图
        - (void)getOriginalPhotoDataWithAsset:(id)asset completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion;
        - (void)getOriginalPhotoDataWithAsset:(id)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(NSData *data,NSDictionary *info,BOOL isDegraded))completion;
      
       /// 获得视频
        - (void)getVideoWithAsset:(id)asset completion:(void (^)(AVPlayerItem * playerItem, NSDictionary * info))completion;
        - (void)getVideoWithAsset:(id)asset progressHandler:(void (^)(double progress, NSError *error, BOOL *stop, NSDictionary *info))progressHandler completion:(void (^)(AVPlayerItem *, NSDictionary *))completion;
        
      
    • 文件读写和剪切
      文件写入一般用于从相册中获取到图片的原图data,然后将其写入到指定的文件夹中,一般调用NSData提供的方法。

      - (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
      

      文件剪切一般用于从相册中获取到视频资源,其视频格式是mov格式的,需要我们视频压缩转成mp4格式,压缩成功后一般将其导入到APP沙盒文件的tmp目录下,总所周知,tmp里面一般存放一些临时文件,所以需要将其导入到Cache文件夹中去,这里用文件移动(剪切)再好不过了,而且不需要读取到内存中去。 直接调用 NSFileManager的提供的API即可:

      - (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error
      

      文件读取一般主要用于读取每一个文件的大小,需要利用NSFileHandle来处理,调用其如下API来完成。

      - (NSData *)readDataOfLength:(NSUInteger)length;
      - (void)seekToFileOffset:(unsigned long long)offset;
      + (nullable instancetype)fileHandleForReadingAtPath:(NSString *)path;
      

      综上所述:NSDataNSFileManagerNSFileHandle的API的常规使用得比较熟练。

    • 视频压缩
      系统的录制视频导出的格式是mov,所以一般的做法就是压缩转化成mp4格式,这样就得用到系统的视频压缩方法,大家可以自行百度AVAssetExportSession的使用。这里笔者采用TZImagePickerController提供的API来做的,具体请参照TZImageManager提供的方法,大家可以看看其实现。

       /// Export video 导出视频 presetName: 预设名字,默认值是AVAssetExportPreset640x480
      - (void)getVideoOutputPathWithAsset:(id)asset success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
      - (void)getVideoOutputPathWithAsset:(id)asset presetName:(NSString *)presetName success:(void (^)(NSString *outputPath))success failure:(void (^)(NSString *errorMessage, NSError *error))failure;
      
    • 资源缓存
      所谓资源缓存,就是一般从系统相册中获取到的资源(图片、视频),我们会将资源另存到在/Library/Caches/Upload目录下,然后把资源存放的相对路径给缓存起来,下次从系统相册中选取相同的资源,如果已经存在于/Library/Caches/Upload目录下,则不需要去获取原始图片,或者压缩视频了。这里笔者采用的是:YYCache 来做内存缓存和磁盘缓存。具体使用,还请自行百度。

    • 数据库
      数据库主要用于,保存新建资源,保存上传资源,保存文件片...等等,利用数据库的等功能,更加方便快捷的处理文件片的上传状态,上传进度,获取或删除草稿数据...等等一些列的操作,大大提供了开发的效率。这里笔者采用的是基于FMDB封装的BGFMDB框架,BGFMDB是对FMDB面相对象层的封装,且几乎支持存储iOS所有基本的自带数据类型,让数据的分别只需要一行代码即可。具体使用,还请查看BGFMDB提供的Demo。

    • 多线程
      多线程的使用主要用在,① 从系统相册获取到资源(图片、视频),对资源进行处理(比如,获取原图,压缩视频等等); ② 文件分片上传。其实现实开发中,我们使用多线程的的场景并不多,但反观使用多线程最多的场景就是--面试。多线程其实是iOS中非常重要的知识点,但是由于平时疏于练习和使用,脑子里面可能只有少许多线程的相关知识。此次笔者在项目中做大文件分片上传功能,也让笔者重拾了多线程的相关知识,而且运用到实际开发中去,也是一个不小的收获。这里笔者就讲讲本模块中用到了哪些多线程的知识,当然具体的理论知识和实践操作,大家可以参照下面笔者分享的网址去针对性的学习和实践多线程的相关知识。具体如下:

      • iOS多线程:『GCD』详尽总结

        特别提醒: ① 必须掌握GCD 队列组:dispatch_group。合理使用dispatch_group_enterdispatch_group_leavedispatch_group_notify的配套使用。
        ② 必须掌握GCD 信号量:dispatch_semaphore。熟练使用dispatch_semaphore_createdispatch_semaphore_signaldispatch_semaphore_wait的配套使用,利用dispatch_semaphore保持线程同步,将异步执行任务转换为同步执行任务以及保证线程安全,为线程加锁。

      • iOS多线程:『NSOperation、NSOperationQueue』详尽总结

    模块

    关于笔者在Demo中提供的文件分片上传的示例程序,虽然不够华丽,但麻雀虽小,五脏俱全,大家凑合着看咯。但总的来说,可以简单分为以下几个模块:

    • 资源新建: 系统相册获取资源文件(图片、视频);获取原图或视频压缩,并导入到沙盒指定的文件夹;资源缓存。

    • 后台接口: 考虑到示例程序中部分业务逻辑是按照后台提供的API设计的,所以有必要分享一下后台提供了哪些API,以及具体的使用的场景。

    • 文件分片: 将新建资源,转化为上传资源,将资源中存放的每一个文件块,按照512k的大小分成若干个文件片。涉及到新建资源存储数据库,上传资源存储数据库,以及每个文件片存储数据库。

    • 草稿存储: 草稿列表的数据来源主要分为手动存草稿自动存草稿手动存草稿一般是指用户手动点击存草稿按钮保存草稿,此草稿数据可以进行二次编辑;自动存草稿一般是指用户点击提交按钮上传资源文件,由于一时半会不会上传到服务器上去,所以需要报存草稿,此草稿数据可以显示上传进度和上传状态,用户可以点击暂停/开始上传此草稿,但不允许二次编辑。当然,草稿数据都是可以手动删除的。

    • 分片上传<核心>: 将上传资源中所有分好的文件片,上传到服务器中去,当网络中断或程序异常都可以支持断点续传,保证在网络中断后该上传资源中已上传的那部分文件片在下次网络连接时或程序启动后不必再重传。涉及到更新资源进度,更新资源状态,以及每一个文件片的上传状态。

    资源新建

    资源新建模块的UI搭建,笔者这里就不过多赘述,这里更多讨论的是功能逻辑和细节处理。具体内容还请查看CMHCreateSourceController.h/m

    • 设置TZImagePickerController导出图片宽度
      默认情况下,TZImagePickerController (PS:后期统称TZ) 默认导出的图片宽度为828px,具体请查看TZ提供的photoWidth属性。考虑到手动存草稿可以是二次编辑,所以有必要把TZ返回的图片储存到数据库中,所以我们只需要存储缩略图即可,何况新建资源模块本身页面也只展示小图,完全没必要导出宽度为828px的图片,这样会导致数据存储和数据读取都异常缓慢,解决方案如下:

      /// CoderMikeHe Fixed Bug : 这里新建模块只需要展示,小图,所以导出图片不需要太大,
      /// 而且导出的图片需要存入数据库,所以尽量尺寸适量即可,否则会导致存储数据库和读取数据库异常的慢
        imagePickerVc.photoWidth = ceil(MH_SCREEN_WIDTH / 4);
      
    • PHAsset 保存数据库
      默认情况下,TZ是支持本地图片预览的,需要我们提供一组selectedAssets,里面装着PHAsset对象,如果我们处于新建资源页面时,这完全没有问题;一旦我们手动存草稿,进行二次编辑时,就会出现问题,原因就是PHAsset不遵守NSCoding协议,无法进行归档。解决方案其实就是储存PHAsset的localIdentifier即可。通过localIdentifier获取PHAsset代码如下:

      /// 获取PHAsset
      PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[file.localIdentifier] options:nil];
      PHAsset *asset = fetchResult.firstObject;
      if (!asset) {
        // 这种场景就是这张照片储存完PHAsset以后,但用户在手机上把这张照片删除
      }
      
    • 资源(图片,视频)处理
      常规逻辑:第一步,通过TZ从系统相册中获取一组资源(图片、视频)文件,第二步,遍历资源列表根据PHAsset去获取原图数据或压缩视频,第三步将处理过的资源保存到Cache/Upload文件夹中。看起来该方案看似稳如藏獒,但是实际情况第二步、第三步操作,其实是非常耗内存的,而且每次获取系统相册中同一个的资源(PHAsset),第二步、第三步处理过后都是一样的,如果该资源(PHAsset)之前已经通过第二步、第三步处理过,那么后面在使用到该资源是不是完全没有必要进行第二步和第三步操作,所以这里就必须用到数据缓存(磁盘缓存+内存缓存)。 最终方案如下:

      资源处理逻辑.png

       

    从上图?明显可知,只有两种场景才会去执行第二步、第三步处理,且都是由于不存在磁盘中导致的。这里有一个比较细节的地方:缓存相对路径。千万不要缓存绝对路径,因为随着APP的更新或重装,都会导致应用的沙盒的绝对路径是会改变的。
    实现代码如下:

    /// 完成图片选中
    - (void)_finishPickingPhotos:(NSArray<UIImage *> *)photos sourceAssets:(NSArray *)assets isSelectOriginalPhoto:(BOOL)isSelectOriginalPhoto infos:(NSArray<NSDictionary *> *)infos{
      
      /// 选中的相片以及Asset
      self.selectedPhotos = [NSMutableArray arrayWithArray:photos];
      self.selectedAssets = [NSMutableArray arrayWithArray:assets];
      /// 记录一下是否上传原图
      self.source.selectOriginalPhoto = isSelectOriginalPhoto;
      
      /// 生成资源文件
      __block NSMutableArray *files = [NSMutableArray array];
      /// 记录之前的源文件
      NSMutableArray *srcFiles = [NSMutableArray arrayWithArray:self.source.files];
      
      NSInteger count = MIN(photos.count, assets.count);
      /// 处理资源
      /// CoderMikeHe Fixed Bug : 这里可能会涉及到选中多个视频的情况,且需要压缩视频的情况
      [MBProgressHUD mh_showProgressHUD:@"正在处理资源..." addedToView:self.view];
      
      NSLog(@"Compress Source Complete Before %@ !!!!" , [NSDate date]);
      
      /// 获取队列组
      dispatch_group_t group = dispatch_group_create();
      /// 创建信号量 用于线程同步
      dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
      
      for (NSInteger i = 0; i < count; i ++ ) {
          dispatch_group_enter(group);
          dispatch_async(_compressQueue, ^{ // 异步追加任务
              /// 设置文件类型
              PHAsset *asset = assets[i];
              /// 图片或资源 唯一id
              NSString *localIdentifier = [[TZImageManager manager] getAssetIdentifier:asset];
              UIImage *thumbImage = photos[i];
              
              /// 这里要去遍历已经获取已经存在资源的文件 内存中
              BOOL isExistMemory = NO;
              for (CMHFile *f in srcFiles.reverseObjectEnumerator) {
                  /// 判断是否已经存在路径和文件
                  if ([f.localIdentifier isEqualToString:localIdentifier] && MHStringIsNotEmpty(f.filePath)) {
                      [files addObject:f];
                      [srcFiles removeObject:f];
                      isExistMemory = YES;
                      break;
                  }
              }
              if (isExistMemory) {
                  NSLog(@"++++ ?文件已经存在内存中? ++++");
                  dispatch_group_leave(group);
              }else{
                  //// 视频和图片,需要缓存,这样会明显减缓,应用的内存压力
                  /// 是否已经缓存在沙盒
                  BOOL isExistCache = NO;
                  
                  /// 1. 先去缓存里面去取
                  NSString *filePath = (NSString *)[[YYCache sharedCache] objectForKey:localIdentifier];
                  /// 这里必须的判断一下filePath是否为空! 以免拼接起来出现问题
                  if (MHStringIsNotEmpty(filePath)) {
                      /// 2. 该路径的本地资源是否存在, 拼接绝对路径,filePath是相对路径
                      NSString * absolutePath = [[CMHFileManager cachesDir] stringByAppendingPathComponent:filePath];
                      if ([CMHFileManager isExistsAtPath:absolutePath]) {
                          /// 3. 文件存在沙盒中,不需要获取了
                          isExistCache = YES;
                          
                          /// 创建文件模型
                          CMHFile *file = [[CMHFile alloc] init];
                          file.thumbImage = thumbImage;
                          file.localIdentifier = localIdentifier;
                          /// 设置文件类型
                          file.fileType = (asset.mediaType == PHAssetMediaTypeVideo)? CMHFileTypeVideo : CMHFileTypePicture;
                          file.filePath = filePath;
                          [files addObject:file];
                      }
                  }
                  
                  
                  if (isExistCache) {
                      NSLog(@"++++ ?文件已经存在磁盘中? ++++");
                      dispatch_group_leave(group);
                  }else{
                      
                      /// 重新获取
                      if (asset.mediaType == PHAssetMediaTypeVideo) {  /// 视频
                          /// 获取视频文件
                          [[TZImageManager manager] getVideoOutputPathWithAsset:asset presetName:AVAssetExportPresetMediumQuality success:^(NSString *outputPath) {
                              NSLog(@"+++ 视频导出到本地完成,沙盒路径为:%@ %@",outputPath,[NSThread currentThread]);
                              /// Export completed, send video here, send by outputPath or NSData
                              /// 导出完成,在这里写上传代码,通过路径或者通过NSData上传
                              /// CoderMikeHe Fixed Bug :如果这样写[NSData dataWithContentsOfURL:xxxx]; 文件过大,会导致内存吃紧而闪退
                              /// 解决办法,直接移动文件到指定目录《类似剪切》
                              NSString *relativePath = [CMHFile moveVideoFileAtPath:outputPath];
                              if (MHStringIsNotEmpty(relativePath)) {
                                  CMHFile *file = [[CMHFile alloc] init];
                                  file.thumbImage = thumbImage;
                                  file.localIdentifier = localIdentifier;
                                  /// 设置文件类型
                                  file.fileType =  CMHFileTypeVideo;
                                  file.filePath = relativePath;
                                  [files addObject:file];
                                  
                                  /// 缓存路径
                                  [[YYCache sharedCache] setObject:file.filePath forKey:localIdentifier];
                              }
                              
                              dispatch_group_leave(group);
                              /// 信号量+1 向下运行
                              dispatch_semaphore_signal(semaphore);
                              
                          } failure:^(NSString *errorMessage, NSError *error) {
                              NSLog(@"???++++ Video Export ErrorMessage ++++??? is %@" , errorMessage);
                              dispatch_group_leave(group);
                              /// 信号量+1 向下运行
                              dispatch_semaphore_signal(semaphore);
                          }];
                      }else{  /// 图片
                          [[TZImageManager manager] getOriginalPhotoDataWithAsset:asset completion:^(NSData *data, NSDictionary *info, BOOL isDegraded) {
                              NSString* relativePath = [CMHFile writePictureFileToDisk:data];
                              if (MHStringIsNotEmpty(relativePath)) {
                                  CMHFile *file = [[CMHFile alloc] init];
                                  file.thumbImage = thumbImage;
                                  file.localIdentifier = localIdentifier;
                                  /// 设置文件类型
                                  file.fileType =  CMHFileTypePicture;
                                  file.filePath = relativePath;
                                  [files addObject:file];
                                  
                                  /// 缓存路径
                                  [[YYCache sharedCache] setObject:file.filePath forKey:localIdentifier];
                              }
                              dispatch_group_leave(group);
                              /// 信号量+1 向下运行
                              dispatch_semaphore_signal(semaphore);
                          }];
                      }
                      /// 等待
                      dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
                  }
              }
          });
      }
      
      /// 所有任务完成
      dispatch_group_notify(group, dispatch_get_main_queue(), ^{
          NSLog(@"Compress Source Complete After %@ !!!!" , [NSDate date]);
          ///
          [MBProgressHUD mh_hideHUDForView:self.view];
          /// 这里是所有任务完成
          self.source.files = files.copy;
          [self.tableView reloadData];
      });
    }
    

    后台接口

    这里分享一下笔者在实际项目中用到的后台提供断点续传的接口,因为项目中部分逻辑处理是根据后台提供的数据来的。这里笔者简单分析一下各个接口的使用场景。

    • 预加载获取文件ID(/fileSection/preLoad.do
      使用场景:根据当次上传的文件数量,预先分配好文件ID,APP终端需要做好保存与文件的对应关系,在续传文件时候作为参数传递。
      请求URL: http://uadmin.xxxx.cn/fileSection/preLoad.do (POST)

      Preload.png

       

    • 断点续传文件(/fileSection/upload.do
      使用场景:大文件分片并行上传。
      请求URL: http://uadmin.xxxx.cn/fileSection/upload.do (POST)

      Upload.png

       

    • 删除文件(/fileSection/delete.do
      使用场景:在App手动删除草稿时同时删除已上传到服务器的文件。
      请求URL: http://uadmin.xxxx.cn/fileSection/delete.do (POST)

      Delete.png

       

    • 检查文件是否上传完毕(/fileSection/isFinish.do
      使用场景:APP中该上传资源的所有的文件片都上传到服务器,服务器需要检查这些文件片的合成情况。如果服务器合成失败,即finishStatus = 0,服务器会把那些合成失败的文件返回给APP,即failFileIds。APP需要根据failFileIds去回滚本地数据库,然后继续重传失败的文件片。
      请求URL: http://uadmin.xxxx.cn/fileSection/isFinish.do (POST)

      finish.png

       

      文件分片

      文件分片的过程主要是在用户点击提交资源的过程。具体内容和细节还请查看CMHSource.h/mCMHFile.h/mCMHFileSource.h/mCMHFileBlock.h/mCMHFileFragment.h/m的实现。

      首先,这里需要将新建资源CMHSource 转成上传资源CMHFileSource,以及将新建资源的文件列表NSArray <CMHFile *> *files转成上传资源的文件块列表NSArray <CMHFileBlock *> *fileBlocks

      其次,需要根据新建资源的文件列表NSArray <CMHFile *> *files的个数,即files.count,去调用后台提供的预加载获取文件ID(/fileSection/preLoad.do)接口,去获取文件ID列表,从而为文件列表NSArray <CMHFile *> *files中每一个文件(CMHFile)绑定文件ID,然后将CMHFile列表转成CMHFileBlock列表,以及将新建资源CMHSource 转成上传资源CMHFileSource。 关键代码如下:

      - (void)commitSource:(void (^)(BOOL))complete{
      
      /// 1. 通过要上传的文件个数  去服务器获取对应的文件ID
      NSInteger uploadFileCount = self.files.count;
      
      /// 2. 以下通过真实的网络请求去模拟获取 文件ID的场景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1
      /// 类似于实际开发中调用服务器的API:  /fileSection/preLoad.do
      /// 1. 配置参数
      CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript];
      subscript[@"isEnglish"] = @0;
      subscript[@"devicetype"] = @2;
      subscript[@"version"] = @"1.0.1";
      
      /// 2. 配置参数模型
      CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary];
      /// 3. 发起请求
      [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id responseObject) {
          /// - 如果到这里了就认为获取文件ID成功,这里模拟后台返回的数据 有几个上传文件 就对应几个上传文件ID
          NSMutableArray *fileIds = [NSMutableArray arrayWithCapacity:uploadFileCount];
          for (NSInteger i = 0; i < uploadFileCount; i++) {
              NSString *fileId = [self _cmh_fileKey];
              [fileIds addObject:fileId];
          }
          /// - 为每个上传文件绑定服务器返回的文件ID,获取要上传的文件块列表
          /// 将服务器文件ID列表转换为,转成json字符串,后期需要存数据库,这个fileIdsStr很重要
          NSString *fileIdsStr = fileIds.yy_modelToJSONString;
          /// 要上传的文件块列表
          NSMutableArray *fileBlocks = [NSMutableArray arrayWithCapacity:uploadFileCount];
          /// 生成上传文件以及绑定文件ID
          for (NSInteger i = 0; i < uploadFileCount; i++) {
              CMHFile *file = self.files[i];
              NSString *fileId = fileIds[i];
              
              /// 资源中的文件绑定文件ID
              file.fileId = fileId;
              
              /// 文件块
              CMHFileBlock *fileBlcok = [[CMHFileBlock alloc] initFileBlcokAtPath:file.filePath fileId:fileId sourceId:self.sourceId];
              [fileBlocks addObject:fileBlcok];
          }
          /// 生成上传文件资源
          CMHFileSource *fileSource = [[CMHFileSource alloc] init];
          fileSource.sourceId = self.sourceId;
          fileSource.fileIds = fileIdsStr;
          fileSource.fileBlocks = fileBlocks.copy;
          /// 保存文件和资源
          /// 非手动存草稿
          self.manualSaveDraft = NO;
          
          /// CoderMikeHe Fixed Bug : 这里必须记录必须强引用上传资源
          self.fileSource = fileSource;
          
          /// 先保存资源
          @weakify(self);
          [self saveSourceToDB:^(BOOL isSuccess) {
              if (!isSuccess) {
                  !complete ? : complete(isSuccess);
                  [MBProgressHUD mh_showTips:@"保存资源失败!!!"];
                  return ;
              }
              @strongify(self);
              /// CoderMikeHe Fixed Bug : 这里必须用self.fileSource 而不是 fileSource ,因为这是异步,会导致 fileSource == nil;
              /// 保存上传资源
              @weakify(self);
              [self.fileSource saveFileSourceToDB:^(BOOL rst) {
                  !complete ? : complete(rst);
                  @strongify(self);
                  /// 这里需要开始上传
                  if (rst) {
                      [[CMHFileUploadManager sharedManager] uploadSource:self.sourceId];
                  }else{
                      [MBProgressHUD mh_showTips:@"保存上传资源失败!!!"];
                  }
              }];
          }];
      
      } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) {
          /// 回调错误
          !complete ? : complete(NO);
          /// show error
          [MBProgressHUD mh_showErrorTips:error];
      }];
      }
      

      然后,我们需要将文件块CMHFileBlock按照512k的大小切割成多个文件片CMHFileFragment,这里的代码实现和属性生成都是参照这篇文章?HTTP断点续传与断点上传之 -- 文件流操作来实现的。关键代码如下:

      // 切分文件片段
      - (void)_cutFileForFragments {
        
        NSUInteger offset = CMHFileFragmentMaxSize;
        // 总片数
        NSUInteger totalFileFragment = (self.totalFileSize%offset==0)?(self.totalFileSize/offset):(self.totalFileSize/(offset) + 1);
        self.totalFileFragment = totalFileFragment;
        NSMutableArray<CMHFileFragment *> *fragments = [[NSMutableArray alloc] initWithCapacity:0];
        for (NSUInteger i = 0; i < totalFileFragment; i ++) {
            
            CMHFileFragment *fFragment = [[CMHFileFragment alloc] init];
            fFragment.fragmentIndex = i;
            fFragment.uploadStatus = CMHFileUploadStatusWaiting;
            fFragment.fragmentOffset = i * offset;
            if (i != totalFileFragment - 1) {
                fFragment.fragmentSize = offset;
            } else {
                fFragment.fragmentSize = self.totalFileSize - fFragment.fragmentOffset;
            }
            
            /// 关联属性
            fFragment.fileId = self.fileId;
            fFragment.sourceId = self.sourceId;
            fFragment.filePath = self.filePath;
            fFragment.totalFileFragment = self.totalFileFragment ;
            fFragment.totalFileSize = self.totalFileSize;
            
            fFragment.fileType = self.fileType;
            fFragment.fileName = [NSString stringWithFormat:@"%@-%ld.%@",self.fileId , (long)i , self.fileName.pathExtension];
            
            
            [fragments addObject:fFragment];
        }
        self.fileFragments = fragments.copy;
      }
      

      最后,我们知道一份上传资源多个文件块组成,而一个文件块多个文件片组成。所以我们是不是可以这样理解:一份上传资源多个文件片组成。前提是要保证每一个文件片,必须含有两个属性sourceIdfileId
      sourceId : 代表这个文件片所属于哪个资源。
      fileId : 代表这个文件片所属于哪个文件块。
      一份上传资源多个文件片组成的代码实现,无非就是重写CMHFileSourcesetFileBlocks即可。关键代码如下:

      - (void)setFileBlocks:(NSArray<CMHFileBlock *> *)fileBlocks{
        _fileBlocks = fileBlocks.copy;
        
        NSMutableArray *fileFragments = [NSMutableArray array];
        
        for (CMHFileBlock *fileBlock in fileBlocks) {
            [fileFragments addObjectsFromArray:fileBlock.fileFragments];
            self.totalFileFragment = self.totalFileFragment + fileBlock.totalFileFragment;
            self.totalFileSize = self.totalFileSize + fileBlock.totalFileSize;
        }
        self.fileFragments = fileFragments.copy;
      }
      

      当然,我们需要将CMHSourceCMHFileSourceCMHFileFragment保存到数据库即可。

    分片上传

    分片上传是本Demo中一个比较重要的功能点,但其实功能点并不难,主要复杂的还是业务逻辑以及数据库处理。分片上传,其原理还是文件上传,某个文件片的上传和我们平时上传头像的逻辑一模一样,不同点无非就是我们需要利用数据库去记录每一片的上传状态罢了。详情请参考:CMHFileUploadManager.h/m

    这里笔者以CMHFileUploadManager上传某个资源为例,具体讲讲其中的逻辑以及细节处理。具体的代码实现请参考:- (void)uploadSource:(NSString *)sourceId;的实现。注意:笔者提供的Demo,一次只能上传一个资源。关于具体的业务逻辑分析,笔者已经写在写在代码注释里面了,这里就不再赘述,还请结合代码注释去理解具体的业务逻辑和场景。关键代码如下:

    /// 上传资源 <核心方法>
    - (void)uploadSource:(NSString *)sourceId{
        
        if (!MHStringIsNotEmpty(sourceId)) { return; }
        
        /// CoderMikeHe Fixed Bug : 解决初次加载的问题,不需要验证网络
        if (self.isLoaded) {
            if (![AFNetworkReachabilityManager sharedManager].isReachable) { /// 没有网络
                [self postFileUploadStatusDidChangedNotification:sourceId];
                return;
            }
        }
        self.loaded = YES;
        
        
        /// - 获取该资源下所有未上传完成的文件片
        NSArray *uploadFileFragments = [CMHFileFragment fetchAllWaitingForUploadFileFragment:sourceId];
        
        if (uploadFileFragments.count == 0) {
            
            /// 没有要上传的文件片
            
            /// 获取上传资源
            CMHFileSource *fileSource = [CMHFileSource fetchFileSource:sourceId];
            /// 获取资源
            CMHSource *source = [CMHSource fetchSource:sourceId];
            
            if (MHObjectIsNil(source)) {
                
                /// 提交下一个资源
                [self _autoUploadSource:sourceId reUpload:NO];
                
                /// 没有资源,则?何须上传资源,将数据库里面清掉
                [CMHFileSource removeFileSourceFromDB:sourceId complete:NULL];
                /// 通知草稿页 删除词条数据
                [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}];
    
                return;
            }
            
            if (MHObjectIsNil(fileSource)) {
                
                /// 提交资源
                [self _autoUploadSource:sourceId reUpload:NO];
                
                /// 没有上传资源 ,则直接提交
                [[CMHFileUploadManager sharedManager] postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES];
                [self _commitSource:sourceId];
                return;
            }
            
            if (fileSource.totalFileFragment <= 0) {
                
                /// 提交资源
                [self _autoUploadSource:sourceId reUpload:NO];
                
                /// 没有上传文件片
                [[CMHFileUploadManager sharedManager] postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES];
                [self _commitSource:sourceId];
                return;
            }
            
            /// 倒了这里 , 证明 fileSource,source 有值,且 fileSource.totalFileFragment > 0
            CMHFileUploadStatus uploadStatus = [CMHFileSource fetchFileUploadStatus:sourceId];
            if (uploadStatus == CMHFileUploadStatusFinished) {
                // 文件全部上传成
                dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.25/*延迟执行时间*/ * NSEC_PER_SEC));
                dispatch_after(delayTime, dispatch_get_main_queue(), ^{
                    /// 检查服务器的文件上传合成状态
                    [self _checkFileFragmentSynthetiseStatusFromService:sourceId];
                });
            }else{
                /// 到了这里,则证明这个草稿永远都不会上传成功了,这里很遗憾则需要将其从数据库中移除
                /// 提交资源
                [self _autoUploadSource:sourceId reUpload:NO];
                
                [CMHSource removeSourceFromDB:sourceId complete:NULL];
                /// 通知草稿页 删除这条数据
                [[NSNotificationCenter defaultCenter] postNotificationName:CMHFileUploadDidFinishedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId}];
            }
            return;
        }
        
        
        /// 0. 这里一定会新建一个新的上传队列,一定会开启一个新的任务
        /// - 看是否存在于上传数组中
        NSString *findSid = nil;
        /// - 是否有文件正在上传
        BOOL isUploading = NO;
        
        for (NSString *sid in self.uploadFileArray) {
            /// 上传资源里面已经存在了,findSid
            if ([sid isEqualToString:sourceId]) {
                findSid = sid;
            }
            /// 查看当前是否有上传任务正在上传
            CMHFileUploadQueue *queue = [self.uploadFileQueueDict objectForKey:sid];
            if (queue && !queue.isSuspended) {
                isUploading = YES;
            }
        }
        
        /// 2. 检查状态,插入数据,
        if (findSid) { /// 已经存在了,那就先删除,后插入到第0个元素
            [self.uploadFileArray removeObject:findSid];
            [self.uploadFileArray insertObject:sourceId atIndex:0];
        }else{ /// 不存在上传资源数组中,直接插入到第0个元素
            [self.uploadFileArray insertObject:sourceId atIndex:0];
        }
        
        /// 3. 检查是否已经有上传任务了
        if (isUploading) { /// 已经有正在上传任务了,则不需要开启队列了,就请继续等待
            /// 发送通知
            [self postFileUploadStatusDidChangedNotification:sourceId];
            return;
        }
        /// 4. 如果没有上传任务,你就创建队里开启任务即可
    
        /// 更新这个上传文件的状态 为 `正在上传的状态`
        [self updateUpLoadStatus:CMHFileUploadStatusUploading sourceId:sourceId];
        
        /// 创建信号量 用于线程同步
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        /// 创建一个队列组
        dispatch_group_t group = dispatch_group_create();
        /// 操作数
        NSMutableArray *operations = [NSMutableArray array];
        
        /// 这里采用串行队列且串行请求的方式处理每一片的上传
        for (CMHFileFragment *ff in uploadFileFragments) {
            /// 进组
            dispatch_group_enter(group);
            // 创建对象,封装操作
            NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
                
                /// 切记:任务(网络请求)是串行执行的 ,但网络请求结果回调是异步的、
                [self _uploadFileFragment:ff
                                 progress:^(NSProgress *progress) {
                                     NSLog(@" \n上传文件ID?【%@】\n上传文件片? 【%ld】\n上传进度为?【%@】",ff.fileId, (long)ff.fragmentIndex, progress.localizedDescription);
                                 }
                                  success:^(id responseObject) {
                                      /// 处理成功的文件片
                                      [self _handleUploadFileFragment:ff];
                                      /// 退组
                                      dispatch_group_leave(group);
                                      /// 信号量+1 向下运行
                                      dispatch_semaphore_signal(semaphore);
                                  } failure:^(NSError *error) {
                                      /// 更新数据
                                      /// 某片上传失败
                                      [ff updateFileFragmentUploadStatus:CMHFileUploadStatusWaiting];
                                      /// 退组
                                      dispatch_group_leave(group);
                                      /// 信号量+1 向下运行
                                      dispatch_semaphore_signal(semaphore);
                                      
                                  }];
                /// 等待
                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            }];
            /// 添加操作数组
            [operations addObject:operation];
        }
        /// 创建NSOperationQueue
        CMHFileUploadQueue * uploadFileQueue = [[CMHFileUploadQueue alloc] init];
        /// 存起来
        [self.uploadFileQueueDict setObject:uploadFileQueue forKey:sourceId];
        /// 把操作添加到队列中 不需要设置为等待
        [uploadFileQueue addOperations:operations waitUntilFinished:NO];
        
        /// 队列组的操作全部完成
        dispatch_group_notify(group, dispatch_get_main_queue(), ^{
            NSLog(@"???+++dispatch_group_notify+++???");
            /// 0. 如果运行到这,证明此`Queue`里面的所有操作都已经全部完成了,你如果再使用 [queue setSuspended:YES/NO];将没有任何意义,所以你必须将其移除掉
            [self.uploadFileQueueDict removeObjectForKey:sourceId];
            /// 1. 队列完毕了,清除掉当前的资源,开启下一个资源
            [self _removeSourceFromUploadFileArray:sourceId];
            /// CoderMikeHe: 这里先不更新草稿页的状态,等提交完表格再去发送通知
            /// 检查一下资源上传
            [self _uploadSourceEnd:sourceId];
        });
        
        //// 告知外界其资源状态改过了
        [self postFileUploadStatusDidChangedNotification:sourceId];
    }
    

    这里对上传资源下的需要上传的文件片做了循环的上传,由于网络请求是一个异步的操作,同时也考虑到太多并发(当然系统对于网络请求开辟的线程个数也有限制)对于手机性能的影响,因此利用GCD信号量等待这种功能特性让一个片段上传完之后再进行下一个片段的上传

    文件上传核心代码如下:

    /// 上传某一片文件 这里用作测试
    - (void)_uploadFileFragment:(CMHFileFragment *)fileFragment
                       progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
                        success:(void (^)(id responseObject))success
                        failure:(void (^)(NSError *error))failure{
        /// 获取上传参数
        NSDictionary *parameters = [fileFragment fetchUploadParamsInfo];
        /// 获取上传数据
        NSData *fileData = [fileFragment fetchFileFragmentData];
        
        /// 资源文件找不到,则直接修改数据库,无论如何也得让用户把资源提交上去,而不是让其永远卡在草稿页里,这样太影响用户体验了
        if (fileData == nil) {
            /// CoderMikeHe Fixed Bug : V1.6.7之前 修复文件丢失的情况
            /// 1. 获取该片所处的资源
            CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:fileFragment.sourceId];
            /// 取出fileID
            NSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject];
            
            NSLog(@"???? Before -- 文件<%@>未找到个数 %ld <%@> ????",fileFragment.fileId , fileIds.count, fileIds);
            if ([fileIds containsObject:fileFragment.fileId]) {
                /// 数据库包含
                [fileIds removeObject:fileFragment.fileId];
                uploadSource.fileIds = fileIds.yy_modelToJSONString;
                /// 更新数据库
                [uploadSource saveOrUpdate];
            }
            NSLog(@"???? After -- 文件<%@>未找到个数 %ld <%@> ????",fileFragment.fileId , fileIds.count, fileIds);
            
            /// 一定要回调为成功,让用户误以为正在上传,而不是直接卡死在草稿页
            NSDictionary *responseObj = @{@"code" : @200};
            !success ? : success(responseObj);
            return;
        }
        
        /// 这里笔者只是模拟一下网络情况哈,不要在乎这些细节 ,
        /// 类似于实际开发中调用服务器的API:  /fileSection/upload.do
        /// 2. 以下通过真实的网络请求去模拟获取 文件ID的场景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1
        /// 1. 配置参数
        CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript];
        subscript[@"isEnglish"] = @0;
        subscript[@"devicetype"] = @2;
        subscript[@"version"] = @"1.0.1";
        
        /// 2. 配置参数模型
        CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary];
        /// 3. 发起请求
        [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id  _Nullable responseObject) {
    #warning CMH TODO 稍微延迟一下,模拟现实情况下的上传进度
            NSInteger randomNum = [NSObject mh_randomNumber:0 to:5];
            [NSThread sleepForTimeInterval:0.1 * randomNum];
            
            !success ? : success(responseObject);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) {
            !failure ? : failure(error);
        }];
    
    #if 0
        /// 这个是真实上传,请根据自身实际项目出发  /fileSection/upload.do
        [self _uploadFileFragmentWithParameters:parameters
                                       fileType:fileFragment.fileType
                                       fileData:fileData
                                       fileName:fileFragment.fileName
                                       progress:uploadProgress
                                        success:success
                                        failure:failure];
    #endif
        
    }
    
    
    /// 实际开发项目中上传每一片文件,这里请结合自身项目开发去设计
    - (NSURLSessionDataTask *)_uploadFileFragmentWithParameters:(NSDictionary *)parameters
                                                       fileType:(CMHFileType)fileType
                                                       fileData:(NSData *)fileData
                                                       fileName:(NSString *)fileName
                                                       progress:(void (^)(NSProgress *))uploadProgress
                                                        success:(void (^)(id responseObject))success
                                                        failure:(void (^)(NSError *error))failure{
        /// 配置成服务器想要的样式
        NSMutableArray *paramsArray = [NSMutableArray array];
        [paramsArray addObject:parameters];
        
        /// 生成jsonString
        NSString *jsonStr = [paramsArray yy_modelToJSONString];
        
        /// 设置TTPHeaderField
        [self.uploadService.requestSerializer setValue:jsonStr forHTTPHeaderField:@"file_block"];
    
        /// 开启文件任务上传
        /// PS : 着了完全可以看成,我们平常上传头像给服务器一样的处理方式
        NSURLSessionDataTask *uploadTask = [self.uploadService POST:@"/fileSection/upload.do" parameters:nil/** 一般这里传的是基本参数 */ constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            
            /// 拼接mimeType
            NSString *mimeType = [NSString stringWithFormat:@"%@/%@",(fileType == CMHFileTypePicture) ? @"image":@"video",[[fileName componentsSeparatedByString:@"."] lastObject]];
            
            /// 拼接数据
            [formData appendPartWithFileData:fileData name:@"sectionFile" fileName:fileName mimeType:mimeType];
            
        } progress:^(NSProgress * progress) {
            !uploadProgress ? : uploadProgress(progress);
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            !success ? : success(responseObject);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            !failure ? : failure(error);
        }];
        return uploadTask;
    }
    

    检查服务器文件上传合成情况的核心代码如下:

    /// 检查服务器文件片合成情况
    - (void)_checkFileFragmentSynthetiseStatusFromService:(NSString *)sourceId{
        
        /// 这里调用服务器的接口检查文件上传状态,以这个为标准
        CMHFileSource *uploadSource = [CMHFileSource fetchFileSource:sourceId];
        /// 没意义
        if (uploadSource == nil) { return; }
        
        /// 如果这里进来了,则证明准备验证文件片和提交表单,则草稿里面的这块表单,你不能在让用户去点击了
        [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:YES];
        
        /// V1.6.5之前的接口老数据
        if (!MHStringIsNotEmpty(uploadSource.fileIds)) {
            /// 这里可能是老数据,直接认为成功,就不要去跟服务器打交道了
            /// 成功
            [self _commitSource:sourceId];
            /// 上传下一个
            [self _autoUploadSource:sourceId reUpload:NO];
            return;
        }
        /// 这里笔者只是模拟一下网络情况哈,不要在乎这些细节,
        /// 类似于实际开发中调用服务器的API:  /fileSection/isFinish.do
        /// 2. 以下通过真实的网络请求去模拟获取 文件ID的场景 https://live.9158.com/Room/GetHotTab?devicetype=2&isEnglish=0&version=1.0.1
        /// 1. 配置参数
        CMHKeyedSubscript *subscript = [CMHKeyedSubscript subscript];
        subscript[@"isEnglish"] = @0;
        subscript[@"devicetype"] = @2;
        subscript[@"version"] = @"1.0.1";
        
        /// 2. 配置参数模型
        CMHURLParameters *paramters = [CMHURLParameters urlParametersWithMethod:CMH_HTTTP_METHOD_GET path:CMH_GET_HOT_TAB parameters:subscript.dictionary];
        
        /// 3. 发起请求
        [[CMHHTTPRequest requestWithParameters:paramters] enqueueResultClass:nil parsedResult:YES success:^(NSURLSessionDataTask *task, id  _Nullable responseObject) {
            
            /// 模拟后台返回的合成结果
            CMHFileSynthetise *fs = [[CMHFileSynthetise alloc] init];
            NSInteger randomNum = [NSObject mh_randomNumber:0 to:20];
            fs.finishStatus = (randomNum > 0) ? 1 : 0;  /// 模拟服务器合成失败的场景,毕竟合成失败的几率很低
            
            if (fs.finishStatus>0) {
                /// 服务器合成资源文件成功
                /// 成功
                [self _commitSource:sourceId];
                /// 上传下一个
                [self _autoUploadSource:sourceId reUpload:NO];
                return ;
            }
            
            /// 服务器合成资源文件失败, 服务器会把合成失败的 fileId 返回出来
            /// 也就是 "failFileIds" : "fileId0,fileId1,..."的格式返回出来
            /// 这里模拟后台返回合成错误的文件ID, 这里只是演习!!这里只是演习!!
            /// 取出fileID
            NSMutableArray *fileIds = [NSMutableArray arrayWithArray:uploadSource.fileIds.yy_modelToJSONObject];
            /// 模拟只有一个文件ID合成失败
            NSString *failFileIds = fileIds.firstObject;
            fs.failFileIds = failFileIds;
            
            /// 这里才是模拟真实的网络情况
            if (MHStringIsNotEmpty(fs.failFileIds)) {
                /// 1. 回滚数据
                [uploadSource rollbackFailureFile:fs.failureFileIds];
                /// 2. 获取进度
                CGFloat progress = [CMHFileSource fetchUploadProgress:sourceId];
                /// 3. 发送通知
                [MHNotificationCenter postNotificationName:CMHFileUploadProgressDidChangedNotification object:nil userInfo:@{CMHFileUploadSourceIdKey : sourceId , CMHFileUploadProgressDidChangedKey : @(progress)}];
                /// 4. 重新设置回滚数据的经度
                [CMHSource updateSourceProgress:progress sourceId:sourceId];
            }else{
                /// 无需回滚,修改状态即可
                [self postFileUploadStatusDidChangedNotification:sourceId];
            }
            
            /// 合成失败,继续重传失败的片,允许用户点击草稿页的资源
            [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO];
            /// 重传该资源
            [self _autoUploadSource:sourceId reUpload:YES];
            
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError *error) {
            /// 1. 服务器报错不重传
            [MBProgressHUD mh_showErrorTips:error];
            
            /// 更新资源状态
            [self updateUpLoadStatus:CMHFileUploadStatusWaiting sourceId:sourceId];
            
            /// 更新状态
            [self postFileUploadStatusDidChangedNotification:sourceId];
            /// 文件片合成失败,允许点击
            [self postFileUploadDisableStatusNotification:sourceId fileUploadDisabled:NO];
        }];
    }
    
    

    总之,文件分片上传逻辑不止上面这一点点内容,还有存在许多逻辑处理和细节注意,比如暂停上传资源;开始上传资源;取消上传资源;取消所有上传资源;服务器合成某些文件失败,客户端回滚数据库,重传失败的文件片;某个资源上传后自动重传下个资源....等等。大家有兴趣可以查看CMHFileUploadManager.h提供的API的具体实现。 CMHFileUploadManager.h的所有内容如下:

    /// 某资源的所有片数据上传,完成也就是提交资源到服务器成功。
    FOUNDATION_EXTERN NSString *const CMHFileUploadDidFinishedNotification;
    /// 资源文件上传状态改变的通知
    FOUNDATION_EXTERN NSString *const CMHFileUploadStatusDidChangedNotification;
    
    /// 草稿上传文件状态 disable 是否不能点击 如果为YES 不要修改草稿页表单的上传状态 主需要让用户不允许点击上传按钮
    FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusKey;
    FOUNDATION_EXTERN NSString *const CMHFileUploadDisableStatusNotification;
    
    /// 某资源中的某片数据上传完成
    FOUNDATION_EXTERN NSString *const CMHFileUploadProgressDidChangedNotification;
    
    /// 某资源的id
    FOUNDATION_EXTERN NSString *const CMHFileUploadSourceIdKey;
    /// 某资源的进度
    FOUNDATION_EXTERN NSString *const CMHFileUploadProgressDidChangedKey;
    
    
    @interface CMHFileUploadManager : NSObject
    
    /// 存放操作队列的字典
    @property (nonatomic , readonly , strong) NSMutableDictionary *uploadFileQueueDict;
    
    /// 声明单例
    + (instancetype)sharedManager;
    
    /// 销毁单例
    + (void)deallocManager;
    
    /// 基础配置,主要是后台上传草稿数据  一般这个方法会放在 程序启动后切换到主页时调用
    - (void)configure;
    
    /// 上传资源
    /// sourceId:文件组Id
    - (void)uploadSource:(NSString *)sourceId;
    
    /// 暂停上传 -- 用户操作
    /// sourceId: 资源Id
    - (void)suspendUpload:(NSString *)sourceId;
    
    /// 继续上传 -- 用户操作
    /// sourceId: 资源Id
    - (void)resumeUpload:(NSString *)sourceId;
    
    /// 取消掉上传 -- 用户操作
    /// sourceId: 资源Id
    - (void)cancelUpload:(NSString *)sourceId;
    
    /// 取消掉所有上传 一般这个方法会放在 程序启动后切换到登录页时调用
    - (void)cancelAllUpload;
    
    /// 删除当前用户无效的资源
    - (void)clearInvalidDiskCache;
    
    //// 以下方法跟服务器交互,只管调用即可,无需回调,
    /// 清除掉已经上传到服务器的文件片 fileSection
    - (void)deleteUploadedFile:(NSString *)sourceId;
    
    /// 告知草稿页,某个资源的上传状态改变
    /// sourceId -- 资源ID
    - (void)postFileUploadStatusDidChangedNotification:(NSString *)sourceId;
    /// 告知草稿页,某个资源不允许点击
    - (void)postFileUploadDisableStatusNotification:(NSString *)sourceId fileUploadDisabled:(BOOL)fileUploadDisabled;
    
    /// 更新资源的状态
    /// uploadStatus -- 上传状态
    /// sourceId -- 资源ID
    - (void)updateUpLoadStatus:(CMHFileUploadStatus)uploadStatus sourceId:(NSString *)sourceId;
    @end
    

    总结

    以上内容,就是笔者在做大文件分片上传的过程中的心得体会。看似简单的文件分片上传功能,但其中涵盖的知识面还是比较广的,结合笔者前面谈及的必备知识点,大家业余时间可以系统去学习和掌握,最后笔者还是建议大家把多线程的相关知识恶补一下和实践起来。当然这其中肯定还有一些细小的逻辑和细节问题还未暴露出来,如果大家在使用和查看过程中发现问题或者不理解的地方,以及如果有好的建议或意见都可以在底部?评论区指出。

    期待

    1. 文章若对您有点帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议?,切记学无止境。
    2. 针对文章所述内容,阅读期间任何疑问;请在文章底部批评指正,我会火速解决和修正问题。
    3. GitHub地址:https://github.com/CoderMikeHe
    4. 源码地址:
      MHDevelopExample目录中的Architecture/Contacts/FileUpload文件夹中 <特别强调: 使用前请全局搜索 CMHDEBUG 字段并将该置为 1即可,默认是0 >



    作者:CoderMikeHe
    链接:https://www.jianshu.com/p/7919c620967e
    來源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

    展开全文
  • 我们当前正处理视频爆炸的时代,各种场景下的视频应用越来越多,而且视频分辨率和视频尺寸越来越大,而且...所以我们在iOS上传内容的时候,需要将大文件进行分片,比如分成1024*1024B,即将大文件分成1M的片进行上...

    我们当前正处理视频爆炸的时代,各种场景下的视频应用越来越多,而且视频分辨率和视频尺寸越来越大,而且我们又需要和他人之间进行视频分享,这样就常常遇到大文件上传和断点续传的问题。因为文件过大(比如2GB以上),必须要考虑上传过程网络中断的情况,需要做到断点续传,否则我们就需要再次重新上传。所以我们在iOS端上传内容的时候,需要将大文件进行分片,比如分成1024*1024B,即将大文件分成1M的片进行上传,服务器端接收后,再将这些分片文件合并成原始文件,这就是分片的基本原理。http的网络请求中已经具备了分片上传功能,当传输的文件比较大时,http协议自动会将文件切片(分块),但这不是我们现在说的重点,我们要做的事是保证在网络中断后2G的文件已上传的那部分在下次网络连通时不必再重传。断点续传要求本地要记录每一片的上传的状态,我通过三个状态进行了标记(wait loading finish),当网络中断,再次连接后,从断点处进行上传。服务器通过文件名、总片数判断该文件是否已全部上传完成。

    而且,对于大的并发应用而言,我们的上传服务器必须支持数百或者数千用户同时上传,这样才能满足运营级的需要。

    在实践中,我们发现很多基于PHP或者Java语言开发的上传服务模块具备基本的上传功能,但是测试后发现无法真正做到断点续传功能,而且支持的并发终端数量只有几十个,没有一个可以真正商用的系统。

    因此,我们为了解决这个问题,花费了很大精力自主用C++语言实现了这一高性能上传服务器,在客户端只要调用一段 js 代码即可,这是一个通用的上传服务模块,可以快速集成到第三方应用平台中。

    项目地址:

    https://github.com/liufeihong/Hyper-Upload-Server

     

    Hyper Upload Server 超级上传服务器简介:

    这是一款超级文件上传服务器,采用异步I/O架构,采用C++语言编码实现。它支持4GB以上超大文件上传和断点续传,支持Windows和Linux服务器平台,支持任意格式的文件上传,尤其适合大的视频网站应用。单台服务器支持1000并发上传进程,支持PC端和智能手机端主流的浏览器。

    主要特性

    1. 服务器端采用异步I/O架设设计,具有高性能I/O处理能力,尤其适用于超大文件上传;

    2. 服务器端采用高效内存分配技术确保在运行过程中服务器的内存开销最小化;

    3. 完全采用标准协议实现,因此兼容几乎所有的PC端和移动端浏览器;

    4. 服务器端采用C++语言自主实现,对上传文件的尺寸无限制,天生支持超大文件上传。

       而基于PHP、JAVA等技术实现的文件上传服务天生无法支持超大文件上传,无法逾越2GB的最大文件尺寸瓶颈;

    5. 服务器端采用无缓冲即时写入方式,上传数据写入一步到位。不同于PHP、JAVA等技术实现方式需要两步写入;

    6. 服务器端可跨平台编译运行,支持Windows和Linux平台;

    7. 高性能,单台服务器支持1000个并发上传进程;

    8. 支持4GB以上超大文件上传,文件大小不受限制;

    9. 客户端支持采用HTTP标准协议上传;

    10.支持断点续传,断网、关机重启均不受影响;

    11.支持HTML5浏览器上传进度实时显示;

    12.支持IE8及以上浏览器上传进度显示;

    13.支持查看客户端在线连接, 查看方法: http://ip:port/lists

    14.多浏览器兼容,包括Chrome,Firefox,Safari,IE,Opera,Edge;

     上传代码实现

    ;
    /*--jquery.md5.js begin--*/
    (function($){

    var rotateLeft = function(lValue, iShiftBits) {
    return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
    }

    var addUnsigned = function(lX, lY) {
        var lX4, lY4, lX8, lY8, lResult;
        lX8 = (lX & 0x80000000);
        lY8 = (lY & 0x80000000);
        lX4 = (lX & 0x40000000);
        lY4 = (lY & 0x40000000);
        lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
        if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
        if (lX4 | lY4) {
        if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
        else return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
        } else {
        return (lResult ^ lX8 ^ lY8);
        }
    }

    var F = function(x, y, z) {
        return (x & y) | ((~ x) & z);
    }

    var G = function(x, y, z) {
        return (x & z) | (y & (~ z));
    }

    var H = function(x, y, z) {
        return (x ^ y ^ z);
    }

    var I = function(x, y, z) {
        return (y ^ (x | (~ z)));
    }

    var FF = function(a, b, c, d, x, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac));
        return addUnsigned(rotateLeft(a, s), b);
    };

    var GG = function(a, b, c, d, x, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac));
        return addUnsigned(rotateLeft(a, s), b);
    };

    var HH = function(a, b, c, d, x, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac));
        return addUnsigned(rotateLeft(a, s), b);
    };

    var II = function(a, b, c, d, x, s, ac) {
        a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac));
        return addUnsigned(rotateLeft(a, s), b);
    };

    var convertToWordArray = function(string) {
        var lWordCount;
        var lMessageLength = string.length;
        var lNumberOfWordsTempOne = lMessageLength + 8;
        var lNumberOfWordsTempTwo = (lNumberOfWordsTempOne - (lNumberOfWordsTempOne % 64)) / 64;
        var lNumberOfWords = (lNumberOfWordsTempTwo + 1) * 16;
        var lWordArray = Array(lNumberOfWords - 1);
        var lBytePosition = 0;
        var lByteCount = 0;
        while (lByteCount < lMessageLength) {
            lWordCount = (lByteCount - (lByteCount % 4)) / 4;
            lBytePosition = (lByteCount % 4) * 8;
            lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition));
            lByteCount++;
        }
        lWordCount = (lByteCount - (lByteCount % 4)) / 4;
        lBytePosition = (lByteCount % 4) * 8;
        lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
        lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
        lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
        return lWordArray;
    };

    var wordToHex = function(lValue) {
        var WordToHexValue = "", WordToHexValueTemp = "", lByte, lCount;
        for (lCount = 0; lCount <= 3; lCount++) {
        lByte = (lValue >>> (lCount * 8)) & 255;
        WordToHexValueTemp = "0" + lByte.toString(16);
        WordToHexValue = WordToHexValue + WordToHexValueTemp.substr(WordToHexValueTemp.length - 2, 2);
        }
        return WordToHexValue;
    };

    var uTF8Encode = function(string) {
        string = string.replace(/\x0d\x0a/g, "\x0a");
        var output = "";
        for (var n = 0; n < string.length; n++) {
        var c = string.charCodeAt(n);
        if (c < 128) {
        output += String.fromCharCode(c);
        } else if ((c > 127) && (c < 2048)) {
        output += String.fromCharCode((c >> 6) | 192);
        output += String.fromCharCode((c & 63) | 128);
        } else {
        output += String.fromCharCode((c >> 12) | 224);
        output += String.fromCharCode(((c >> 6) & 63) | 128);
        output += String.fromCharCode((c & 63) | 128);
        }
        }
        return output;
    };

    $.extend({
    md5: function(string) {
    var x = Array();
    var k, AA, BB, CC, DD, a, b, c, d;
    var S11=7, S12=12, S13=17, S14=22;
    var S21=5, S22=9 , S23=14, S24=20;
    var S31=4, S32=11, S33=16, S34=23;
    var S41=6, S42=10, S43=15, S44=21;
    string = uTF8Encode(string);
    x = convertToWordArray(string);
    a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
    for (k = 0; k < x.length; k += 16) {
    AA = a; BB = b; CC = c; DD = d;
    a = FF(a, b, c, d, x[k+0], S11, 0xD76AA478);
    d = FF(d, a, b, c, x[k+1], S12, 0xE8C7B756);
    c = FF(c, d, a, b, x[k+2], S13, 0x242070DB);
    b = FF(b, c, d, a, x[k+3], S14, 0xC1BDCEEE);
    a = FF(a, b, c, d, x[k+4], S11, 0xF57C0FAF);
    d = FF(d, a, b, c, x[k+5], S12, 0x4787C62A);
    c = FF(c, d, a, b, x[k+6], S13, 0xA8304613);
    b = FF(b, c, d, a, x[k+7], S14, 0xFD469501);
    a = FF(a, b, c, d, x[k+8], S11, 0x698098D8);
    d = FF(d, a, b, c, x[k+9], S12, 0x8B44F7AF);
    c = FF(c, d, a, b, x[k+10], S13, 0xFFFF5BB1);
    b = FF(b, c, d, a, x[k+11], S14, 0x895CD7BE);
    a = FF(a, b, c, d, x[k+12], S11, 0x6B901122);
    d = FF(d, a, b, c, x[k+13], S12, 0xFD987193);
    c = FF(c, d, a, b, x[k+14], S13, 0xA679438E);
    b = FF(b, c, d, a, x[k+15], S14, 0x49B40821);
    a = GG(a, b, c, d, x[k+1], S21, 0xF61E2562);
    d = GG(d, a, b, c, x[k+6], S22, 0xC040B340);
    c = GG(c, d, a, b, x[k+11], S23, 0x265E5A51);
    b = GG(b, c, d, a, x[k+0], S24, 0xE9B6C7AA);
    a = GG(a, b, c, d, x[k+5], S21, 0xD62F105D);
    d = GG(d, a, b, c, x[k+10], S22, 0x2441453);
    c = GG(c, d, a, b, x[k+15], S23, 0xD8A1E681);
    b = GG(b, c, d, a, x[k+4], S24, 0xE7D3FBC8);
    a = GG(a, b, c, d, x[k+9], S21, 0x21E1CDE6);
    d = GG(d, a, b, c, x[k+14], S22, 0xC33707D6);
    c = GG(c, d, a, b, x[k+3], S23, 0xF4D50D87);
    b = GG(b, c, d, a, x[k+8], S24, 0x455A14ED);
    a = GG(a, b, c, d, x[k+13], S21, 0xA9E3E905);
    d = GG(d, a, b, c, x[k+2], S22, 0xFCEFA3F8);
    c = GG(c, d, a, b, x[k+7], S23, 0x676F02D9);
    b = GG(b, c, d, a, x[k+12], S24, 0x8D2A4C8A);
    a = HH(a, b, c, d, x[k+5], S31, 0xFFFA3942);
    d = HH(d, a, b, c, x[k+8], S32, 0x8771F681);
    c = HH(c, d, a, b, x[k+11], S33, 0x6D9D6122);
    b = HH(b, c, d, a, x[k+14], S34, 0xFDE5380C);
    a = HH(a, b, c, d, x[k+1], S31, 0xA4BEEA44);
    d = HH(d, a, b, c, x[k+4], S32, 0x4BDECFA9);
    c = HH(c, d, a, b, x[k+7], S33, 0xF6BB4B60);
    b = HH(b, c, d, a, x[k+10], S34, 0xBEBFBC70);
    a = HH(a, b, c, d, x[k+13], S31, 0x289B7EC6);
    d = HH(d, a, b, c, x[k+0], S32, 0xEAA127FA);
    c = HH(c, d, a, b, x[k+3], S33, 0xD4EF3085);
    b = HH(b, c, d, a, x[k+6], S34, 0x4881D05);
    a = HH(a, b, c, d, x[k+9], S31, 0xD9D4D039);
    d = HH(d, a, b, c, x[k+12], S32, 0xE6DB99E5);
    c = HH(c, d, a, b, x[k+15], S33, 0x1FA27CF8);
    b = HH(b, c, d, a, x[k+2], S34, 0xC4AC5665);
    a = II(a, b, c, d, x[k+0], S41, 0xF4292244);
    d = II(d, a, b, c, x[k+7], S42, 0x432AFF97);
    c = II(c, d, a, b, x[k+14], S43, 0xAB9423A7);
    b = II(b, c, d, a, x[k+5], S44, 0xFC93A039);
    a = II(a, b, c, d, x[k+12], S41, 0x655B59C3);
    d = II(d, a, b, c, x[k+3], S42, 0x8F0CCC92);
    c = II(c, d, a, b, x[k+10], S43, 0xFFEFF47D);
    b = II(b, c, d, a, x[k+1], S44, 0x85845DD1);
    a = II(a, b, c, d, x[k+8], S41, 0x6FA87E4F);
    d = II(d, a, b, c, x[k+15], S42, 0xFE2CE6E0);
    c = II(c, d, a, b, x[k+6], S43, 0xA3014314);
    b = II(b, c, d, a, x[k+13], S44, 0x4E0811A1);
    a = II(a, b, c, d, x[k+4], S41, 0xF7537E82);
    d = II(d, a, b, c, x[k+11], S42, 0xBD3AF235);
    c = II(c, d, a, b, x[k+2], S43, 0x2AD7D2BB);
    b = II(b, c, d, a, x[k+9], S44, 0xEB86D391);
    a = addUnsigned(a, AA);
    b = addUnsigned(b, BB);
    c = addUnsigned(c, CC);
    d = addUnsigned(d, DD);
    }
    var tempValue = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
    return tempValue.toLowerCase();
    }
    });
    })(jQuery); 
    /*--jquery.md5.js end--*/

    /*--- HYUpload.js-begin--*/

    function toast(msg,duration)
    {
        var toastDiv = document.getElementById('hyupload-toast');
        if (msg) {
            toastDiv.style.display = 'block';
            toastDiv.innerHTML = msg;
            duration = parseInt(duration);
            if (!duration) duration = 3000;
            if (!window.uploadtoast) {
                window.clearTimeout(window.uploadtoast);
            }
            window.uploadtoast = window.setTimeout(function() {
                toastDiv.style.display = 'none';
                delete window.uploadtoast;
            },duration);
        }
        else {
            toastDiv.style.display = 'none';
            toastDiv.innerHTML = '';
        }
    }

    Array.prototype.in_array=function(e){
        var S=String.fromCharCode(2);
        var r=new RegExp(S+e+S);
        return (r.test(S+this.join(S)+S));
    };

    var HYFileUploader = function (options) {
        var _options = {
            filelist:'hyupload-filelist',
            notice_url:'',
            savepaths:{'video':'.mp4,.mkv,.avi,.wmv','document':'.zip,.rar','image':'.jpg,.jpeg,.png,.gif','audio':'.aac,.mp3'},
            ontaskfinish:function(taskobj) {
            
            },
            ontaskprogress:function(taskobj) {
            
            },
            ontaskprogress:function(progress) {
            
            },
            ontaskstart:function(taskobj) {
                return true;
            }
        };
        
        $.extend(_options, options);
        
        //简单的Cookie帮助函数
        var setCookie = function(cname,cvalue,exdays) {
            var d = new Date();
            d.setTime(d.getTime()+(exdays*24*60*60*1000));
            var expires = "expires="+d.toGMTString();
            document.cookie = cname + "=" + cvalue + "; " + expires;
        };

        var getCookie = function(cname) {
            var name = cname + "=";
            var ca = document.cookie.split(';');
            for(var i=0; i<ca.length; i++) 
            {
                var c = ca[i].trim();
                if (c.indexOf(name)==0) return c.substring(name.length,c.length);
            }
            return "";
        };
        
        //
        //简单的文件HASH值计算,如果您不是十分考究,应该可以用于产品。
        //由于计算文件HASH值用到了多种数据,因此在HYFileUploader系统范围内发生HASH冲突的可能性应该非常小,应该可以放心使用。
        //获取文件的ID可以用任何算法来实现,只要保证做到同一文件的ID是相同的即可,获取的ID长度不要超过32字节
        //
        var getFileId = function (file) {
            //给浏览器授予一个唯一的ID用于区分不同的浏览器实例
            var clientid = getCookie("HUAYIUPLOAD");
            if (clientid == "") {
                //用一个随机值来做浏览器的ID,将作为文件HASH值的一部分
                var rand = parseInt(Math.random() * 1000);
                var t = (new Date()).getTime();
                clientid =rand+'T'+t;
                
                setCookie("HUAYIUPLOAD",clientid,365);
            }
            
            var info = clientid;
            if (file.lastModified)
                info += file.lastModified;
            if (file.name)
                info += file.name;
            if (file.size)
                info += file.size;
            //https://cdn.bootcss.com/blueimp-md5/2.10.0/js/md5.min.js
            var fileid = $.md5(info);
            return fileid;
        };
        
        var get_file_mimetype = function(filename) {
            var mimeType;
            var extPos = filename.lastIndexOf('.');
            if (extPos != -1) {
                var extName = filename.substr(extPos).toLowerCase();
                if (extName == '.rm' || extName == '.rmvb') {
                    mimeType = 'video/realvideo';
                }
                else if (extName == '.dat') {
                    mimeType = 'video/vcd';
                }
                else if (extName == '.ra') {
                    mimeType = 'audio/realaudio';
                }
            }
            
            if (!mimeType) mimeType = 'bin/unknown';
            return mimeType;
        };

        //savepaths:{'video':'.mp4,*.mkv,.avi,.wmv','document':'.zip,*.rar','image':'.jpg,.jpeg,.png,.gif','audio':'.aac,.mp3'}
        var get_save_path = function(filename,savepaths) {
            var extPos = filename.lastIndexOf('.');
            if (extPos != -1) {
                var extName = filename.substr(extPos).toLowerCase();
                
                for (path in savepaths) {
                    var extList = savepaths[path].split(',');
                    if (extList.indexOf(extName) != -1) {
                        return path;
                    }
                }
            }
            
            return '';
        }
        
        var bytesToSize = function (bytes) {
            if (bytes === 0) return '0 B';
            if (bytes < 1024) return (bytes + 'B');
            var k = 1024,sizes = ['B','KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
            var i = Math.floor(Math.log(bytes) / Math.log(k));
            return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
        }

        var _addFileToList = function (listViewer,fileobj) {
            var fileSources = '';
            var fileid = '';
            var filename = '',filesize = 0;
            var fileitemId = '';
            
            var $listViewer = $(listViewer);
            
            if (fileobj instanceof Array) {
                fileSources = '<filesources>';
                
                for(var i = 0; i < fileobj.length; i ++) {
                    if (filename != '') filename += '|';
                    var file = fileobj[i];
                    
                    filename += file.name;
                    filesize += file.size;
                    
                    fileid = getFileId(file);
                    
                    fileSources += '<file id="'+fileid+'" fname="'+file.name+'" fsize="'+file.size+'"></file>';
                    
                    fileitemId += fileid;
                    
                    file.fileid = fileid;
                }
                fileSources += '</filesources>';
                
                fileitemId = $.md5(fileitemId);
            }
            else {
                var typename = typeof fileobj;
                
                if (typename != 'object') {
                    return false;
                }
                
                if (fileobj.size == 0) {
                    toast('文件 “'+fileobj.name+'” 长度为0无法上传,忽略此文件。');
                    return false;
                }
                fileid = getFileId(fileobj);
                
                filename = fileobj.name;
                filesize = fileobj.size;
                fileSources = '<filesources><file id="'+fileid+'" fname="'+fileobj.name+'" fsize="'+fileobj.size+'"></file></filesources>';
                
                fileitemId = fileid;
                
                fileobj.fileid = fileid;
            }
            
            var file_existed = false;
                
            $listViewer.children('div.upload-fileitem').each(function(index,obj) {
                if (obj.id == fileitemId) {
                    file_existed = true;
                    return false;
                }
            });
            
            if (file_existed) {
                toast('添加的文件或文件组合 “'+filename+'” 已经存在,不重复添加');
                
                return false;
            }
                
            if (!listViewer.uploadOptions) {
                toast('添加的文件 “'+filename+'” 失败,文件列表未绑定上传对象。');
                
                return false;
            }
            
            //移除提示元素
            $listViewer.children('div.fileupload-help').remove();
            $listViewer.children('.notice').remove();
                            
            var server = listViewer.uploadOptions.server;
            var cid = listViewer.uploadOptions.cid;
            var serverid = listViewer.uploadOptions.serverid;
            var userid = listViewer.uploadOptions.userid;
            
            var html = '<div class="upload-fileitem" id="'+fileitemId+'" data-server="'+server+'" data-cid="'+cid+'" data-serverid="'+serverid+'" data-userid="'+userid+'">';
            html += fileSources;
            html += '<a class="remove_icon enabled" href="javascript:void(0);" title="移除"></a><a class="edit_icon enabled" href="javascript:void(0);" title="编辑"><i class="icon-edit"></i></a><i class="icon-ok-circle"></i>';
            html += '<div class="fileinfo"><div class="filename"><span title="'+filename+'">'+filename+'</span></div>';
            html += '<div class="upload-status"><div class="item"><span class="title">长度:</span><span class="value filesize" data-id="filesize">';
            html += bytesToSize(filesize);
            html += '</span></div>';
            html += '<div class="item"><span class="title">已传:</span><span class="value finish" data-id="finish" title="实际已经上传的文件数据长度">0</span></div>';
            html += '<div class="item"><span class="title">速率:</span><span class="value bitrate" data-id="bitrate">0</span></div>';
            html += '</div></div>';
            html += '<div class="progressbar"><div class="xprogressbar progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">';
            html += '<div class="progress-track progress-success" style="width: 0%;"></div></div><span class="percent-label text" data-id="percent-label">0%</span></div></div>';
            
            var $fileItem = $(html);
            $fileItem.appendTo($listViewer);
            
            //$('#upload-filelistview').mCustomScrollbar('update');
            
            $fileItem.find('a.remove_icon').click(function(evt) {
                evt.preventDefault();
                if (!$(this).hasClass('enabled')) {
                    return;
                }
                var $item = $(this).parent();
                if ($item.attr('data-upload')=='finished') {
                    toast('文件已经上传完成,不必移除,如要清空队列,请强制刷新浏览器。');
                    return;
                }
                //remove_icon 的外层 upload-fileitem
                var fileItemDomObj = $item.get(0);
                
                var fileViewer = $item.get(0).fileViewer;
                if (fileViewer.uploader) {
                    var uploader = fileViewer.uploader;
                    
                    if ($item.attr('data-upload')=='uploading') {
                        if (!uploader.options.forceremove) {
                            if (!confirm('视频文件正在上传,确定要放弃吗?')) {
                                return;
                            }
                        }
                                    
                        //再次检查是否还在上传,如果没有启动上传uploader 属性无值
                        
                        if (uploader.curFileitemElement && uploader.curFileitemElement == fileItemDomObj) {
                            //仅仅执行取消上传操作
                            if (uploader.currentFile) {
                                if (uploader.currentXHR && typeof uploader.currentXHR.abort == 'function') {
                                    if (uploader.options.forceremove) {
                                        uploader.taskIsForceRemove = true;
                                    }
                                    
                                    uploader.currentXHR.abort();
                                    return ;
                                }
                            }
                        }
                    }
                }
                            
                if (fileItemDomObj.fileObject) {
                    fileItemDomObj.fileObject = null;
                    delete fileItemDomObj.fileObject;
                }
                $item.remove();
                
                var $fileitems = $(fileViewer).children('.upload-fileitem');
                if ($fileitems.length == 0) {
                    $(fileViewer).html('<p class="notice">暂无上传任务</p>');
                }
                
                //$('#upload-filelistview').mCustomScrollbar('update');
            });
            
            $fileItem.find('a.edit_icon').click(function(evt) {
                evt.preventDefault();
            });
                
            var fileItemDom = $fileItem.get(0);
            var fileObject = {
                taskid:fileitemId,
                files:fileobj,
                totalfilesize:filesize,
                uploadedsize:0,
                finished:0
            }
            
            fileItemDom.fileObject = fileObject;
            fileItemDom.fileViewer = listViewer;
            
            return fileid;
        };
        //fileInfo 是上传服务器返回的
        var _onUploadedFileFinished = function(fileInfo) {
            var uploader = this;
            
            //fileInfo 对象的值可以用于保存到CMS里,而不仅仅是用于显示
            if (uploader.options.show_output && uploader.uploadResult) {
                var html = '<div class="file-object" >';
                    html += '<div class="info-row"><span class="rlabel">文件名:</span><span class="rvalue">'+fileInfo.name+'</span></div>';
                    html += '<div class="info-row"><span class="rlabel">保存路径:</span><span class="rvalue">'+fileInfo.path+'</span></div>';
                    html += '<div class="info-row"><span class="rlabel">文件尺寸:</span><span class="rvalue">'+fileInfo.filesize+'</span></div>';
                    html += '<div class="info-row"><span class="rlabel">访问URL:</span><span class="rvalue">'+fileInfo.url+'</span></div>';
                html += '</div>';
                //将文件信息显示在列表里
                uploader.uploadResult.innerHTML +=html;
            }
            
            uploader.currentXHR = null;
            
            if (uploader.curFileitemElement) {
                var $fileNode = $(uploader.curFileitemElement);
                
                var cid = $fileNode.attr('data-cid');
                var serverid = $fileNode.attr('data-serverid');
                var userid = $fileNode.attr('data-userid');
                
                uploader.currentFile.uploadInfo = fileInfo;
                
                var taskid = uploader.curFileitemElement.id;
                var mimeType=mimeType = uploader.currentFile.type;
                    
                if (!mimeType) {
                    mimeType = get_file_mimetype(fileInfo.name);
                }
                            
                var title = uploader.curFileitemElement.videotitle ? curFileitemElement.videotitle:'';
                var desc = uploader.curFileitemElement.videodesc ? curFileitemElement.videodesc:'';
                //记录文件完成上传
                
                var uploadEvent = {
                    "event":"filefinished",
                    "taskid":uploader.curFileitemElement.id,
                    "fileid":uploader.currentFile.fileid,
                    "cid":cid,
                    "serverid":serverid,
                    "userid":userid,
                    "mimetype":mimeType,
                    "filepath":fileInfo.path,
                    "filename":fileInfo.name,
                    "filesize":fileInfo.filesize,
                    "url":fileInfo.url,
                    "title":title,
                    "desc":desc
                };
                            
                if (uploader.options.notice_url) {
                    $.post(uploader.options.notice_url,uploadEvent,function(data) {
                        if (data.code != 1) {
                            toast(data.msg);
                        }
                    },'json');
                }
                
                if (typeof uploader.options.onevent == 'function') {
                    uploader.options.onevent.call(uploader, uploadEvent);
                }
                            
                var isTaskfinish = 1;
                
                if (uploader.curFileitemElement.fileObject.files instanceof Array) {
                    for(var i = 0; i < uploader.curFileitemElement.fileObject.files.length; i ++) {
                        if (!uploader.curFileitemElement.fileObject.files[i].uploadInfo) {
                            isTaskfinish = 0;
                            break;
                        }
                    }
                }
                else {
                    isTaskfinish = 1;
                }
                
                uploader.currentFile = null;
                uploader.upload_start = false;
                
                if (isTaskfinish) {
                    $fileNode.attr('data-upload','finished').addClass('finished');
                    $fileNode.find('.remove_icon').removeClass('enabled');
                    
                    uploader.curFileitemElement.fileObject.finished = 1;
                                    
                    if (typeof uploader.options.ontaskfinish == 'function') {
                        uploader.options.ontaskfinish.call(uploader,uploader.curFileitemElement.fileObject);
                    }
                    
                    uploader.curFileitemElement = null;
                    //接着启动下一个任务
                    if (!_startNextTaskupload.call(uploader)) {
                        if (uploader.startbutton) {
                            uploader.startbutton.disabled=false;
                            $(uploader.startbutton).removeClass('disabled');
                            uploader.startbutton = null;
                        }
                    }
                }
                else {
                    //继续上传本任务的下一个文件
                    _startFileupload.call(uploader,false);
                }                
            }
        }

        /*
            文件上传处理代码
            fileObj : html5 File 对象
            start_offset: 上传的数据相对于文件头的起始位置
            fileid: 文件的ID,这个是上面的getFileId 函数获取的,
        */
        var _do_upload_file = function (fileObj,start_offset,fileid) {
            var uploader = this;
            
            var xhr = new XMLHttpRequest();
            var formData = new FormData();
            
            var blobfile;
            
            if(start_offset >= fileObj.size){
                return false;
            }
            
            var bitrateDiv = null;
            var finishDiv = null;
            var progressBar = null;
            var progressDiv = null;

            if (uploader.curFileitemElement) {
                var $fileItem = $(uploader.curFileitemElement);
                
                bitrateDiv = $fileItem.find('.bitrate').get(0);
                finishDiv = $fileItem.find('.finish').get(0);
                progressBar = $fileItem.find('.progress-track').get(0);
                progressDiv = $fileItem.find('.percent-label').get(0);
            
            }
            else {
                bitrateDiv = document.getElementById("bitrate");
                finishDiv = document.getElementById("finish");
                progressBar = document.getElementById('progressbar');
                progressDiv = document.getElementById('percent-label');
            }

            var oldTimestamp = 0;
            var oldLoadsize = 0;
            var totalFilesize = uploader.curFileitemElement.fileObject.totalfilesize;
            if (totalFilesize == 0) {
                
                return false;
            }
            //将fileObj 设置为当前正在上传的文件
            uploader.currentFile = fileObj;
            //已经完成了的文件的总尺寸
            var totalUploadedsize = uploader.curFileitemElement.fileObject.uploadedsize;
            
            var uploadProgress = function (evt) {
                if (evt.lengthComputable) {
                    var uploadedSize = totalUploadedsize + evt.loaded + start_offset; 
                    var percentComplete = Math.round(uploadedSize * 100 / totalFilesize);

                    var timestamp = (new Date()).valueOf();
                    var isFinish = evt.loaded == evt.total;

                    if (timestamp > oldTimestamp || isFinish) {
                        var duration = timestamp - oldTimestamp;
                        if (duration > 500 || isFinish) {
                            var size =  evt.loaded - oldLoadsize;

                            var bitrate = (size * 8 / duration /1024) * 1000; //kbps
                            if (bitrate > 1000)
                                bitrate = Math.round(bitrate / 1000) + 'Mbps';
                            else
                                bitrate = Math.round(bitrate) + 'Kbps';

                            var finish = totalUploadedsize + evt.loaded + start_offset;

                            if (finish > 1048576)
                                finish = (Math.round(finish / (1048576/100)) / 100).toString() + 'MB';
                            else
                                finish = (Math.round(finish / (1024/100) ) / 100).toString() + 'KB';

                            progressBar.style.width = percentComplete+'%';
                            progressDiv.innerHTML = percentComplete.toString() + '%';
                            bitrateDiv.innerHTML = bitrate;
                            finishDiv.innerHTML = finish;

                            oldTimestamp = timestamp;
                            oldLoadsize = evt.loaded;
                            
                            if (typeof uploader.options.ontaskprogress == 'function') {
                                var progress = {
                                    'bitrate':bitrate,
                                    'finish':finish,
                                    'percent':percentComplete
                                };
                                
                                uploader.options.ontaskprogress.call(uploader,progress);
                            }
                        }
                    }
                }
                else {
                    progressDiv.innerHTML = 'N/A';
                }
            }
            
            xhr.onreadystatechange = function(){
                if (xhr.readyState == 4 && xhr.status == 200) {
                    if (console)
                        console.log( xhr.responseText );
                }
                else if (xhr.status == 400) {
                
                }
            };

            var uploadComplete = function (evt) {
                progressDiv.innerHTML = '100%';
                
                uploader.curFileitemElement.fileObject.uploadedsize += uploader.currentFile.size;
                
                var result = JSON.parse(evt.target.responseText);
                if (result.result == 'success') {
                    _onUploadedFileFinished.call(uploader,result.files[0]);
                }
                else {
                    uploader.currentFile = null;
                    uploader.upload_start = false;
                
                    toast(result.msg);
                }
            }

            var uploadFailed = function (evt) {
                if (!uploader.currentFile) return;
                if (uploader.reconnectId) return;
                uploader.upload_start = false;
                
                toast("检测到网络故障!两秒后尝试重连...");
                        
                uploader.reconnectId = window.setTimeout(function() {
                    _startFileupload.call(uploader,true);
                },2000);
                
                if (typeof uploader.options.ontaskfail == 'function') {
                    uploader.options.ontaskfail.call(uploader,uploader.curFileitemElement.fileObject);
                }
            }

            var uploadCanceled = function (evt) {
                            
                if (uploader.curFileitemElement) {
                    $(uploader.curFileitemElement).attr('data-upload','aborted');
                    
                    
                    var fileid = uploader.currentFile.fileid;
                    //记录文件取消上传
                    
                    var uploadEvent = {
                        "event":'abortupload',
                        "taskid":uploader.curFileitemElement.id,
                        "fileid":fileid
                    };
                
                    if (uploader.options.notice_url) {
                        $.post(uploader.options.notice_url,uploadEvent,function(data) {
                            if (data.code != 1) {
                                toast(data.msg);
                            }
                        },'json');
                    }
                    
                    if (typeof uploader.options.onevent == 'function') {
                        uploader.options.onevent.call(uploader, uploadEvent);
                    }
                    if (uploader.taskIsForceRemove) {
                        delete uploader.curFileitemElement.fileObject;
                        $(uploader.curFileitemElement).remove();
                        uploader.curFileitemElement = null;
                        
                        var $fileitems = $(uploader.fileViewer).children('.upload-fileitem');
                        if ($fileitems.length == 0) {
                            $(uploader.fileViewer).html('<p class="notice">暂无上传任务</p>');
                        }
                    }
                }
                
                uploader.currentFile = null;
                uploader.upload_start = false;
                
                if (uploader.startbutton) {
                    uploader.startbutton.disabled=false;
                    $(uploader.startbutton).removeClass('disabled');
                    uploader.startbutton = null;
                }
                
                if (uploader.taskIsForceRemove) {
                    uploader.currentXHR = null;
                    
                    delete uploader.taskIsForceRemove;
                    //在可以强制删除的情况下继续执行下一个任务,但不提示toast
                    _startNextTaskupload.call(uploader,true);
                }
                else {
                    toast("上传已经被暂停取消或者浏览器断开了连接!");
                    if (typeof uploader.options.onpause == 'function') {
                        uploader.options.onpause.call(uploader);
                    }
                }
            }
            
            
            //设置超时时间,由于是上传大文件,因此千万不要设置超时
            //xhr.timeout = 20000;
            //xhr.ontimeout = function(event){
            //        alert('文件上传时间太长,服务器在规定的时间内没有响应!');
            //}         

            xhr.overrideMimeType("application/octet-stream"); 

            var mimeType = fileObj.type;
                    
            if (!mimeType) {
                mimeType = get_file_mimetype(fileObj.name);
            }
                    
            //附加的文件数据应该放在请求的前面
            var cid = uploader.curFileitemElement.getAttribute('data-cid');
            var serverid = uploader.curFileitemElement.getAttribute('data-serverid');
            formData.append('channelid', cid);
            formData.append('filename', fileObj.name);
            //必须将fileid信息传送给服务器,服务器只有在获得了fileid信息后才对文件做断点续传处理
            formData.append('fileid', fileid);
            //请将文件数据放在最后的域
            var filesize = fileObj.size;
            var blob = fileObj.slice(start_offset,filesize);
            if('msSaveOrOpenBlob' in navigator){
                formData.append("file",blob, fileObj.name);
            }
            else {
                var fileOfBlob = new File([blob], fileObj.name);
                formData.append('file', fileOfBlob);
            }
                
            xhr.upload.addEventListener("progress", uploadProgress, false);
            
            xhr.addEventListener("load", uploadComplete, false);
            xhr.addEventListener("error", uploadFailed, false);
            xhr.addEventListener("abort", uploadCanceled, false);
            
            var upload_url_full = uploader.upload_file_url + get_save_path(fileObj.name,uploader.options.savepaths);
            
            xhr.open('POST', upload_url_full);
            //
            xhr.send(formData);
            uploader.currentXHR = xhr;
            uploader.upload_start = true;
            toast('');
            if (start_offset == 0) {
                
                var uploadEvent = {
                    "event":'startupload',
                    "fileid":fileid,
                    "cid":cid,
                    "serverid":serverid,
                    "filename":fileObj.name,
                    "mimetype":mimeType
                };
                
                //开始文件上传
                if (uploader.options.notice_url) {
                    $.post(uploader.options.notice_url,uploadEvent,function(data) {
                        if (data.code != 1) {
                            toast(data.msg);
                        }
                    },'json');
                }
                
                if (typeof uploader.options.onevent == 'function') {
                    uploader.options.onevent.call(uploader, uploadEvent);
                }
            }
            else {
                //记录文件断点续传
                var uploadEvent = {
                    "event":'resumeupload',
                    "fileid":fileid,
                    "cid":cid,
                    "serverid":serverid,
                    "filename":fileObj.name,
                    "mimetype":mimeType
                };
                
                if (uploader.options.notice_url) {
                    $.post(uploader.options.notice_url,uploadEvent,function(data) {
                        if (data.code != 1) {
                            toast(data.msg);
                        }
                    },'json');
                }
                
                if (typeof uploader.options.onevent == 'function') {
                    uploader.options.onevent.call(uploader, uploadEvent);
                }
            }
        }
        /*
            处理上传服务器直接返回的上传完成信息
        */
        var _do_finish_file = function (fileObj,fileInfo) {
            var uploader = this;
            
            var bitrateDiv = null;
            var finishDiv = null;
            var progressBar = null;
            var progressDiv = null;

            if (!uploader.curFileitemElement) {
                return;
            }
            
            var $fileItem = $(uploader.curFileitemElement);
            
            bitrateDiv = $fileItem.find('.bitrate').get(0);
            finishDiv = $fileItem.find('.finish').get(0);
            progressBar = $fileItem.find('.progress-track').get(0);
            progressDiv = $fileItem.find('.percent-label').get(0);
            
            //模拟进度完成
            
            var totalFilesize = uploader.curFileitemElement.fileObject.totalfilesize;
            if (totalFilesize == 0) {
                return false;
            }
            //将fileObj 设置为当前正在上传的文件
            uploader.currentFile = fileObj;
            //已经完成了的文件的总尺寸
            var totalUploadedsize = uploader.curFileitemElement.fileObject.uploadedsize;
            
            var uploadedSize = totalUploadedsize + fileObj.size; 
            var percentComplete = Math.round(uploadedSize * 100 / totalFilesize);
                    
            var finish = uploadedSize;

            if (finish > 1048576)
                finish = (Math.round(finish / (1048576/100)) / 100).toString() + 'MB';
            else
                finish = (Math.round(finish / (1024/100) ) / 100).toString() + 'KB';

            progressBar.style.width = percentComplete+'%';
            progressDiv.innerHTML = percentComplete.toString() + '%';
            bitrateDiv.innerHTML = '';
            finishDiv.innerHTML = finish;
            
            uploader.curFileitemElement.fileObject.uploadedsize += fileObj.size;
            
            _onUploadedFileFinished.call(uploader,fileInfo);
        }

        //doupload
        var _startFileupload = function (bReconnect) {
            var uploader = this;
            
            if (!this.options) {
                alert("发生未知错误,没有正确绑定上传对象。")
                return false;
            }
            
            if (!this.curFileitemElement) {
                alert("请选择文件后再试!")
                return false;
            }
            
            if (!bReconnect) {
                if (uploader.currentFile) {
                    alert("上传对象已经有文件在上传了,不要重复调用!")
                    return false;
                }
            }
            
            var fileObject = this.curFileitemElement.fileObject;
            if (fileObject.finished) {
                return false;
            }
            
            var fileObj = null;
            
            if (!this.currentFile) {
                            
                if (this.curFileitemElement.fileObject.files instanceof Array) {
                    for(var i = 0; i < this.curFileitemElement.fileObject.files.length; i ++) {
                        if (!this.curFileitemElement.fileObject.files[i].uploadInfo) {
                            fileObj = this.curFileitemElement.fileObject.files[i];
                            break;
                        }
                    }
                }
                else {
                    if (!this.curFileitemElement.fileObject.files.uploadInfo) {
                        fileObj = this.curFileitemElement.fileObject.files;
                    }
                }
                
                if (!fileObj) {
                    alert("任务文件已经上传完毕!")
                    return false;
                }
                
                //this.currentFile = fileObj;
            }
            else {
                fileObj = this.currentFile;
            }
            
            var fileid = getFileId(fileObj);
            var t = (new Date()).getTime();
            //通过以下URL获取文件的断点续传信息,必须的参数为fileid,后面追加t参数是避免浏览器缓存
            var url = this.resume_info_url + '?fileid='+fileid + '&t='+t;
            
            var ajax = new XMLHttpRequest();
            
            ajax.onerror = function (e) {
                if (uploader.reconnectId) 
                    return;
                
                uploader.reconnectId = window.setTimeout(function() {
                    uploader.currentXHR = null;
                    
                    _startFileupload.call(uploader,true);
                },2000);
            };
            
            ajax.onabort = function (evt) {
                uploader.currentFile = null;
                uploader.upload_start = false;
                
                if (uploader.curFileitemElement) {
                    $(uploader.curFileitemElement).attr('data-upload','aborted');
                    
                    var fileid = uploader.curFileitemElement.id;
                            
                    uploader.curFileitemElement = null;
                }
                if (uploader.startbutton) {
                    uploader.startbutton.disabled=false;
                    $(uploader.startbutton).removeClass('disabled');
                    uploader.startbutton = null;
                }
                            
                toast("上传被取消或者浏览器断开了连接!");
            }
            
            ajax.onreadystatechange = function () {
                
                if(this.readyState == 4){
                    if (bReconnect) {
                        //目前是重连状态,清除重连标志
                        uploader.reconnectId = 0;
                    }
                
                    if (this.status == 200){
                        var response = this.responseText;
                        
                        var result = JSON.parse(response);
                        if (!result) {
                            alert('服务器返回的数据不正确,可能是不兼容的服务器,上传无法继续!');
                            return;
                        }
                        //断点续传信息返回的文件对象包含已经上传的尺寸
                        var uploadedBytes = result.file && result.file.size;
                        if (!result.file.finished && uploadedBytes < fileObj.size) {
                            _do_upload_file.call(uploader,fileObj,uploadedBytes,fileid);
                        }
                        else {
                            //文件已经上传完成了,就不要再上传了,直接返回结果就可以了
                            _do_finish_file.call(uploader,fileObj,result.file);
                        }
                    }else {
                        uploader.currentFile = null;
                        uploader.currentXHR = false;
                
                        toast('获取文件断点续传信息失败,服务器错误码:'+this.status+', 上传无法继续!');
                    }  
                }
            }

            if (typeof this.options.ontaskstart == 'function') {
                if (false === this.options.ontaskstart.call(this,this.curFileitemElement.fileObject)) {
                    return false;
                }
            }
            
            ajax.open('get',url,true);
            ajax.send(null);
            this.currentXHR = ajax;
            
            return true;
        }

        //任务上传完成后,开启下一个任务
        function _startNextTaskupload(isAfterAbort) {
            var uploader = this;
            var $fileitems = $(this.fileViewer).children('.upload-fileitem');
            $fileitems.each(function(index,elm) {
                if ($(elm).attr('data-upload') != 'finished') {
                    if (elm.fileObject) {
                        $(elm).attr('data-upload','uploading');
                        uploader.curFileitemElement = elm;
                        return false;
                    }
                }
            });
            
            if (!uploader.curFileitemElement) {
                
                if (typeof this.options.onend == 'function') {
                    this.options.onend.call(this);
                }
                if (!isAfterAbort) {            
                    toast('所有文件已经上传完毕!');
                }
                return false;
            }
            
            _startFileupload.call(uploader,false);
            
            return true;
        }

        
        var fileViewer = document.getElementById(_options.filelist);
        if (!fileViewer) {
            return null;
        }
        
        fileViewer.uploadOptions = _options;
        
        if (_options.enabledrop) {
            //禁止浏览器打开文件的默认行为
            document.addEventListener("drop",function(e){//拖离 
                e.preventDefault();
            });
            document.addEventListener("dragleave",function(e){//拖后放 
                e.preventDefault();
            });
            document.addEventListener("dragenter",function(e){//拖进
                e.preventDefault();
            });
            document.addEventListener("dragover",function(e){//拖来拖去  
                e.preventDefault();
            });

            fileViewer.addEventListener("drop",function (e) {
                var fileList = e.dataTransfer.files; //获取文件对象
                if (console) console.log(fileList)
                //检测是否是拖拽文件到页面的操作
                if(fileList.length == 0){
                    return false;
                }
                
                for(var i = 0; i < fileList.length; i++) {
                    _addFileToList(this,fileList[i]);
                }
            },false);
        }
        
        if (_options.fileselector) {
            var fileSelector = document.getElementById(_options.fileselector);
            if (fileSelector) {
                fileSelector.fileViewer = fileViewer;
                
                fileSelector.addEventListener("change",function (e) {
                    var fileViewer = this.fileViewer;
                    
                    if (window.File && window.FileList) {
                        if (this.files.length) {
                            
                            for(var i = 0; i < this.files.length; i++) {
                                _addFileToList(fileViewer,this.files[i]);
                            }
                            
                            this.value = '';
                        }
                    }
                    else {
                        window.alert('抱歉,你的浏览器不支持FileAPI,请升级浏览器!');
                    }
                },false);
            }
        }
        
        return {
            options:_options,
            fileViewer: fileViewer,
            resume_info_url:'',
            upload_file_url:'',
            upload_start:false,
            startbutton:null,
            currentFile: null,  //当前正在上传的文件对象
            curFileitemElement : null, //当前正在上传的 文件 DOM元素
            
            start:function(btnObj) {
                if (this.upload_start) {
                    alert("文件上传正在进行中,请稍候再点击重复长传!")
                    return false;
                }
            
                var $fileitems = $(this.fileViewer).children('.upload-fileitem');
                if ($fileitems.length == 0) {
                    toast('没有任何可以上传的文件,请先添加文件再试!');
                    return false;
                }
                
                var uploader = this;
                this.fileViewer.uploader = this;
                
                $fileitems.each(function(index,elm) {
                    if ($(elm).attr('data-upload') != 'finished') {
                        if (elm.fileObject) {
                            $(elm).attr('data-upload','uploading');
                            uploader.curFileitemElement = elm;
                            
                            var uploadsrv_addr = 'http://'+uploader.options.server;
                            uploader.resume_info_url = uploadsrv_addr+'/resume/';
                            uploader.upload_file_url = uploadsrv_addr+'/upload/';
                
                            return false;
                        }
                    }
                });
                
                if (!this.curFileitemElement) {
                    toast('所有文件已经上传完毕,请添加新文件后再试!');
                    return false;
                }

                this.startbutton = btnObj;
                
                if (this.options.authize_url) {
                    //如果提供了授权验证URL,则向CMS平台进行验证
                    $.get(this.options.authize_url,{"t":(new Date).getTime()},function(result) {
                        if (result.code != 1) {
                            if (typeof this.options.onend == 'function') {
                                this.options.onend.call(this);
                            }
                            toast(result.msg);
                            return;
                        }
                        
                        if (_startFileupload.call(uploader,false)) {
                            if (uploader.startbutton) {
                                uploader.startbutton.disabled=true;
                                $(uploader.startbutton).addClass('disabled');
                            }
                        }
                        else {
                            if (typeof this.options.onend == 'function') {
                                this.options.onend.call(this);
                            }
                        }
                    },'json');
                    
                    return true;
                }
                else {
                    if (_startFileupload.call(uploader,false)) {
                        if (uploader.startbutton) {
                            uploader.startbutton.disabled=true;
                            $(uploader.startbutton).addClass('disabled');
                        }
                        return true;
                    }
                    return false;
                }
            },
            
            pause:function () {
                if (this.currentFile) {
                    if (this.currentXHR && typeof this.currentXHR.abort == 'function') {
                        this.currentXHR.abort();
                    }
                }
            },
            
            addtask:function(fileobjs) {
                _addFileToList(this.fileViewer,fileobjs);
            },
            
            removetask:function(taskid) {
                var $fileItemElm = $('#'+taskid);
                if ($fileItemElm.length == 1 && $fileItemElm.hasClass('upload-fileitem')) {
                    if ($fileItemElm.attr('data-upload')=='uploading') {
                        alert('任务正在进行中,请停止任务后再删除!');
                        return;
                    }
                    
                    var itemDom = $fileItemElm.get(0);
                    if (itemDom.fileObject) {
                        delete itemDom.fileObject;
                    }
                    
                    $fileItemElm.remove();
                }
            },
            
            clear:function() {
                var $fileitems = $(this.fileViewer).children('.upload-fileitem');
                if ($fileitems.length == 0) {
                    return false;
                }
                
                if (!confirm('确实要清除任务列表吗?')) {
                    return false;
                }
                
                if (this.currentFile) {
                    if (this.currentXHR && typeof this.currentXHR.abort == 'function') {
                        this.currentXHR.abort();
                    }
                    
                    if (typeof this.options.onend == 'function') {
                        this.options.onend.call(this);
                    }
                }
                
                $fileitems.each(function(index,elm) {
                    if (elm.fileObject) delete elm.fileObject;
                });
                
                $fileitems.remove();
                this.curFileitemElement = null;
                this.currentFile = null;
                this.currentXHR = null;
                            
                $(this.fileViewer).html('<p class="notice">暂无上传任务</p>');
            },
                    
            version: "2.0"
        };
    };

     

    技术交流

    - QQ:1918098288

    - Mail: 1918098288@qq.com
    --------------------- 
    作者:mediadev 
    来源:CSDN 
    原文:https://blog.csdn.net/mediadev/article/details/94433671 
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • 而提单离不开文件上传,接下来详细介绍一下ios文件上传ios文件上传可以通过NSURLConnection、NSURLSession、AFN等方式上传,因为不想依赖于第三方框架,我这里选用了NSURLConnection 1、文件上传设置请求体的...

    一个完整的自动化工具,自动提单是非常重要的一环,bug单可以作为衡量自动化工具的产出指标,可以直接说明该自动化工具的价值。而提单离不开文件上传,接下来详细介绍一下ios的文件上传。
    ios文件上传可以通过NSURLConnection、NSURLSession、AFN等方式上传,因为不想依赖于第三方框架,我这里选用了NSURLConnection

    1、文件上传设置请求体的数据格式:

      //请求体拼接格式
      //分隔符:----WebKitFormBoundaryhBDKBUWBHnAgvz9c
     
      //01.文件参数拼接格式
     
       --分隔符
       Content-Disposition:参数
       Content-Type:参数
       空行
       文件参数
     
      //02.非文件拼接参数
       --分隔符
       Content-Disposition:参数
       空行
       非文件的二进制数据
     
      //03.结尾标识
      --分隔符--
    

    2、NSURLConnection的方式

    举一个具体的例子,表单有两个参数,file_name和file_path,file_name表示文件的名字,file_path表示文件的内容,上传的表单格式如下:
    在这里插入图片描述
    在ios中的拼接格式如下:

    /*
     1. 设置请求头
     Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryOhB08CzI96Eux6PO
     
     2. 按照固定的格式拼接请求体的数据
     ------WebKitFormBoundaryOhB08CzI96Eux6PO
    Content-Disposition: form-data; name="file_path"; filename="xx.zip"
    Content-Type: application/octet-stream
     空行
     文件参数
     
     ------WebKitFormBoundaryOhB08CzI96Eux6PO
     Content-Disposition: form-data; name="file_name"
    空行
    xx.zip
     ------WebKitFormBoundaryOhB08CzI96Eux6PO--
     */
     3.结尾表示
     --分隔符--
     */
    

    3、具体代码如下

    -(void)uploadBugWithURL:(NSURL*_Nullable)furl complete:(UploadBugBoolBlock _Nullable)complete synchronous:(BOOL)synchronous{
        
        NSString *filename = [furl lastPathComponent] ;
        
        //1. 确定请求路径
        NSURL *url = [NSURL URLWithString:@"https://test.haleli.com/bugs/api/upload_file/"];
        //2. 创建可变的请求对象
        NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
        //3. 设置请求方法
        requestM.HTTPMethod = @"POST";
        
        //4. 设置请求头信息
        //Authorization:Token 123456789\r\n;
        [requestM setValue:[NSString stringWithFormat:
                            @"Token 123456789\r\n" ] forHTTPHeaderField:@"Authorization"];
        
        //Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryOhB08CzI96Eux6PO
        [requestM setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",Kboundary] forHTTPHeaderField:@"Content-Type"];
        
        
        //5. 设置请求体数据
        NSMutableData *fileData = [NSMutableData data];
        //5.1 文件参数
        /*
         --分隔符
         Content-Disposition: form-data; name="file_path"; filename="xx.zip"
         Content-Type: application/octet-stream
         空行
         文件参数
         */
        [fileData appendData:[[NSString stringWithFormat:@"--%@",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
        // 拼接换行
        [fileData appendData:KNewLine];
        
        //name:file_path 服务器规定的参数
        //filename:xx.zip 文件保存到服务器上面的名称
        //Content-Type:文件的类型
        [fileData appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"file_path\"; filename=\"%@\"",filename] dataUsingEncoding:NSUTF8StringEncoding]];
        [fileData appendData:KNewLine];
        [fileData appendData:[@"Content-Type: application/octet-stream" dataUsingEncoding:NSUTF8StringEncoding]];
        [fileData appendData:KNewLine];
        [fileData appendData:KNewLine];
        
        //文件数据部分
        // NSURL --> NSData
        NSData *uploadData = [NSData dataWithContentsOfURL:furl];
        [fileData appendData:uploadData];
        [fileData appendData:KNewLine];
        
        //5.2 非文件参数
        /*
         --分隔符
         Content-Disposition: form-data; name="file_name"
         空行
         xx.zip
         */
        [fileData appendData:[[NSString stringWithFormat:@"--%@",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
        [fileData appendData:KNewLine];
        [fileData appendData:[@"Content-Disposition: form-data; name=\"file_name\"" dataUsingEncoding:NSUTF8StringEncoding]];
        [fileData appendData:KNewLine];
        [fileData appendData:KNewLine];
        [fileData appendData:[filename dataUsingEncoding:NSUTF8StringEncoding]];
        [fileData appendData:KNewLine];
        
        //5.3 结尾标识
        /*
         --分隔符--
         */
        [fileData appendData:[[NSString stringWithFormat:@"--%@--",Kboundary] dataUsingEncoding:NSUTF8StringEncoding]];
        
        //6. 设置请求体
        requestM.HTTPBody = fileData;
        
        //7. 发送请求
        if(synchronous){
            //同步请求
            BOOL isMock = [[LLDebugTool sharedTool] mockSwitch] ;
            if(isMock){
                [[LLMockHelper sharedHelper] stopMock] ;
            }
            NSData *data= [NSURLConnection sendSynchronousRequest:requestM returningResponse:nil error:nil] ;
            NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding] ;
            NSLog(@"haleli >>> %@",result);
            NSDictionary *dict = [LLTool dictWithJsonString:result] ;
            NSNumber *ret = [dict objectForKey:@"success"] ;
            [self performBoolComplete:complete ret:ret filePath:furl.path] ;
            if(isMock){
                [[LLMockHelper sharedHelper] startMock] ;
            }
        }else{
            //异步请求
            BOOL isMock = [[LLDebugTool sharedTool] mockSwitch] ;
            if(isMock){
                [[LLMockHelper sharedHelper] stopMock] ;
            }
            [NSURLConnection sendAsynchronousRequest:requestM queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
                
                //8.解析数据
                NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding] ;
                NSLog(@"haleli >>> %@",result);
                NSDictionary *dict = [LLTool dictWithJsonString:result] ;
                NSNumber *ret = [dict objectForKey:@"success"] ;
                [self performBoolComplete:complete ret:ret filePath:furl.path] ;
                
                if(isMock){
                    [[LLMockHelper sharedHelper] startMock] ;
                }
            }];
        }
    }
    
    

    4、参考文章

    1、https://blog.csdn.net/m0_37989980/article/details/78859046
    2、http://lysongzi.com/2016/01/28/iOS-%E4%BD%BF%E7%94%A8NSURLConnection%E8%BF%9B%E8%A1%8CHttp-Get%E5%92%8CPost%E8%AF%B7%E6%B1%82%E7%BD%91%E7%BB%9C%E6%95%B0%E6%8D%AE/
    3、https://www.jianshu.com/p/db0e1843c425
    4、https://www.jianshu.com/p/44629e5bf986
    5、https://blog.csdn.net/wudj810818/article/details/50903416
    6、http://www.voidcn.com/article/p-gbonskld-yz.html

    展开全文
  • iOS AFNetWorking上传文件

    2015-06-02 09:28:42
    文中的 XXXX根据你自己的参数填写 - (void)uploadImageWithImage:(NSString *)... //上传其他所需参数  NSString *userId=XXXXXXXXXXX;  NSString *token=XXXXXXXXXXX;    //上传请求POST  
  • 源码描述了AFNetworking的下载、请求,当上传不能完成,没有网址。
  • 在http网络请求中,post没有请求长度的限制,因为post把数据放在了body中,而不是像Get一样放在了浏览器的地址栏中(可以这么理解),所以相对安全。POST有两种方式第一种直接把数据放
  • 在分享经验之前,先说点题外话,之前的一个项目涉及到了多图片的上传,本来以为是一个很简单的事情,却着实困扰了我好久,究其原因,一是我不够细心,二是与后台人员的交流不够充分。在此,我想将我的老师常说的一句...
  • 数组里面接收到的图片 有uiimage 和NSCFstring两种形式的图片 吧nscfstring形式转成image就可以了 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.requestSerializer = ...
  • 我经常使用「多看」和「掌阅」App 看书,其中有一个共同的功能就是 WiFi 传书,根据 App 的提示在电脑浏览器打开指定的地址,传入文件就可以直接发送到手机上阅读了。虽然这个功能需求不是很多,但是也对其进行了...
  • 0.导入框架准备工作  •1. 将AFNetworking3.0+框架程序拖拽进项目   •2. 或使用Cocopod 导入AFNetworking3.0+   •3. 引入 #import "AFNetworking.h"   1.UI准备工作  ...
  • iOS上传表单数据

    2016-05-20 20:05:28
     从事iOS开发以来,一直都是用自己封装的AF请求,先讲一下以前的做法,上传图片时,通常都是用 这个方法。上传的图片信息就直接放在formData的block块中 现在要携带一个"userid"和"image"的参数,我都是写一个...
  • ios 上传图片截图和视频预览 的时候一直说是尺寸错误,但是都是用手机截的图,懵逼… 只能让ui去改图片尺寸 App 预览规范 (iOS、tvOS) 您可以提供 tH.264 和 ProRes 422(仅限 HQ)格式的 App 预览,并且需要...
  • 我想要跟踪的增量上载进度,但我找不到这样做 2.0 版的示例。我的应用程序是 iOS 7,所以我已经选择了为 AFHTTPSessionManager。 任何人都可以提供如何修改这段上传进度进行跟踪的示例? AFHTTPSessionManager *...
  • PHP 上传文件接口: //保存图片 $json_result ['status'] = 0; $path = 'upfile'; $json_result ['status'] = 0; $json_result ['successmsg'] = '上传失败'; if (isset ( $_FILES ['image'] )) { $upfile ...
  • from: http://www.jianshu.com/p/9d9e3699515e @明天不用上课Logo写文章 注册登录首页下载App搜索iOS开发证书与配置文件的使用96 明天不用上课 关注...开发iOS应用必须要有iOS证书(Certificates)和配置文件(Provisioni
  • ios通过post上传文件

    2015-02-02 16:09:43
    由于iOS无法通过html表单来上传图片,因此想要上传图片,必须实现http请求,而不能像其他语言那样通过html表单的post就能上传。  上传图片的http post请求的格式是这样的:  Java代码  Content-...
  • AF上传图片问题

    2014-11-30 16:58:21
    前几天用AF作网络请求,在上传图片的时候遇到了一些问题。。 - (void)postRequsetUploadImage:(NSString *)url Dic:(NSDictionary *)dic image:(UIImage *)image { NSString * strUrl = [HostAddress ...
1 2 3 4 5 ... 20
收藏数 1,811
精华内容 724
热门标签