org.xutils.http.loader.FileLoader

org.xutils.http.loader.FileLoader

package org.xutils.http.loader;



import android.text.TextUtils;



import org.xutils.cache.DiskCacheEntity;

import org.xutils.cache.DiskCacheFile;

import org.xutils.cache.LruDiskCache;

import org.xutils.common.Callback;

import org.xutils.common.util.IOUtil;

import org.xutils.common.util.LogUtil;

import org.xutils.common.util.ProcessLock;

import org.xutils.ex.FileLockedException;

import org.xutils.ex.HttpException;

import org.xutils.http.RequestParams;

import org.xutils.http.request.UriRequest;



import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.io.UnsupportedEncodingException;

import java.net.URLDecoder;

import java.util.Arrays;

import java.util.Date;



/**

* @author 注释者:王教成

* @version 注释版:1.0.0

* 文件加载器

* 下载参数策略:

* 1.RequestParams#saveFilePath不为空时,目标文件保存在saveFilePath;否则由Cache策略分配文件下载路径

* 2.下载时临时目标文件路径为tempSaveFilePath,下载完后进行a:CacheFile#commit;b:重命名等操作

* 断点下载策略:

* 1.要下载的目标文件不存在或小于CHECK_SIZE时删除目标文件,重新下载

* 2.若文件存在且大于CHECK_SIZE,range=fileLen-CHECK_SIZE,校验check_buffer,相同:继续下载;不相同:删掉目标文件,并抛出RuntimeException(HttpRetryHandler会使下载重新开始)

*/

public class FileLoader extends Loader<File> {

private static final int CHECK_SIZE = 512;//创建检查尺寸512

   private String tempSaveFilePath;//声明临时保存文件路径

   private String saveFilePath;//声明保存文件路径

   private boolean isAutoResume;//是否自动恢复

   private boolean isAutoRename;//是否自动重命名

   private long contentLength;//声明内容长度

   private String responseFileName;//声明响应文件名称

   private DiskCacheFile diskCacheFile;//声明磁盘缓存文件



   /**

    * 创建新实例

    * @return 返回文件加载器

    */

   @Override

   public Loader<File> newInstance() {

return new FileLoader();

   }



/**

    * 设置请求参数

    * @param params 请求参数

    */

   @Override

   public void setParams(final RequestParams params) {

if (params != null) {

this.params = params;//如果请求参数非空则赋值

           isAutoResume = params.isAutoResume();//获取请求参数是否自动恢复

           isAutoRename = params.isAutoRename();//获取请求参数是否自动重命名

       }

}



/**

    * 加载输入流

    * @param in 输入流

    * @return 返回文件

    * @throws Throwable 抛出异常

    */

   @Override

   public File load(final InputStream in) throws Throwable {

File targetFile = null;//声明目标文件

       BufferedInputStream bis = null;//声明缓冲输入流

       BufferedOutputStream bos = null;//声明缓冲输出流

       try {

targetFile = new File(tempSaveFilePath);//以临时保存文件路径创建目标文件

           if (targetFile.isDirectory()) {

IOUtil.deleteFileOrDir(targetFile);//如果目标文件是目录,删除目标文件目录

           }//防止文件正在写入时,父文件夹被删除,继续写入时造成偶现文件节点异常问题

           if (!targetFile.exists()) {

File dir = targetFile.getParentFile();//如果目标文件不存在,获取目标文件父目录

               if (!dir.exists() && !dir.mkdirs()) {

throw new IOException("can not create dir: " + dir.getAbsolutePath());//如果父目录不存在且未创建,抛出输入输出异常

               }

}



//处理【断点逻辑2】(见文件头doc)代码块

           long targetFileLen = targetFile.length();//获取目标文件长度

           if (isAutoResume && targetFileLen > 0) {

FileInputStream fis = null;//如果自动恢复且目标文件长度大于0,声明文件输入流

               try {

long filePos = targetFileLen - CHECK_SIZE;//目标文件长度减512检查尺寸,作为文件位置

                   if (filePos > 0) {

fis = new FileInputStream(targetFile);//如果文件位置位于0,以目标文件创建文件输入流

                       byte[] fileCheckBuffer = IOUtil.readBytes(fis, filePos, CHECK_SIZE);//用文件输入流创建文件检查缓冲字节数组

                       byte[] checkBuffer = IOUtil.readBytes(in, 0, CHECK_SIZE);//用输入流创建检查缓冲字节数组

                       if (!Arrays.equals(checkBuffer, fileCheckBuffer)) {//先关闭文件流,否则文件删除会失败

                           IOUtil.closeQuietly(fis);//如果字节数组比较不相同,关闭文件输入流

                           IOUtil.deleteFileOrDir(targetFile);//删除目标文件

                           throw new RuntimeException("need retry");//抛出运行时异常

                       } else {

contentLength -= CHECK_SIZE;//如果字节数组比较相同,内容长度叠减检查尺寸512

                       }

} else {

IOUtil.deleteFileOrDir(targetFile);//如果文件位置不大于0,删除目标文件

                       throw new RuntimeException("need retry");//抛出运行时异常

                   }

} finally {

IOUtil.closeQuietly(fis);//关闭文件输入流

               }

}



//开始下载代码块

           long current = 0;//创建当前初始值0

           FileOutputStream fileOutputStream = null;//声明文件输出流

           if (isAutoResume) {

current = targetFileLen;//如果自动恢复,目标文件长度赋值当前值

               fileOutputStream = new FileOutputStream(targetFile, true);//用目标文件创建文件输出流(true表示附加)

           } else {

fileOutputStream = new FileOutputStream(targetFile);//如果不自动回复,用目标文件创建文件输出流

           }



long total = contentLength + current;//内容长度加上当前值作为总计

           bis = new BufferedInputStream(in);//用输入流创建缓冲输入流

           bos = new BufferedOutputStream(fileOutputStream);//用文件输出流创建缓冲输出流



           if (progressHandler != null && !progressHandler.updateProgress(total, current, true)) {

throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新进度失败,抛出已取消异常

           }



byte[] tmp = new byte[4096];//创建4k缓存字节数组

           int len;//声明长度

           while ((len = bis.read(tmp)) != -1) {

if (!targetFile.getParentFile().exists()) {

targetFile.getParentFile().mkdirs();//如果从缓冲输入流读取缓存长度不等于-1则迭代,如果目标文件父目录不存在,创建父目录

                   throw new IOException("parent be deleted!");//抛出输入输出异常

               }//防止父文件夹被其他进程删除,继续写入时造成父文件夹变为0字节文件的问题



               bos.write(tmp, 0, len);//写入缓存输出流

               current += len;//叠加已读写长度

               if (progressHandler != null) {

if (!progressHandler.updateProgress(total, current, false)) {

bos.flush();//如果进度操作器非空且更新进度失败,刷新且强制输出缓存输入流

                       throw new Callback.CancelledException("download stopped!");//抛出已取消异常

                   }

}

}

bos.flush();//刷新且强制输出缓存输入流



           //处理【下载逻辑2.a】(见文件头doc)代码块

           if (diskCacheFile != null) {

targetFile = diskCacheFile.commit();//如果磁盘缓存文件非空,提交作为目标文件

           }



if (progressHandler != null) {

progressHandler.updateProgress(total, current, true);//如果进度操作器非空,更新进度

           }

} finally {

IOUtil.closeQuietly(bis);//关闭缓冲输入流

           IOUtil.closeQuietly(bos);//关闭缓冲输出流

       }



return autoRename(targetFile);//返回自动重命名目标文件

   }



/**

    * 加载Uri请求参数

    * @param request Uri请求参数

    * @return 返回文件

    * @throws Throwable 抛出异常

    */

   @Override

   public File load(final UriRequest request) throws Throwable {

ProcessLock processLock = null;//声明进度锁

       File result = null;//声明结果文件

       try {

//处理【下载逻辑1】(见文件头doc)代码块

           saveFilePath = params.getSaveFilePath();//请求参数获取保存文件路径

           diskCacheFile = null;//磁盘缓存文件初始为null

           if (TextUtils.isEmpty(saveFilePath)) {

if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {

throw new Callback.CancelledException("download stopped!");//如果保存文件路径非空,并且进度操作器非空且更新进度失败,抛出已取消异常

               }

initDiskCacheFile(request);//以Uri请求参数初始化磁盘缓存文件;保存路径为空,存入磁盘缓存

           } else {

tempSaveFilePath = saveFilePath + ".tmp";//如果保存文件路径为空,加后缀成为缓存保存文件路径

           }



if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {

throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新失败,抛出已取消异常

           }



processLock = ProcessLock.tryLock(saveFilePath + "_lock", true);//获取进度锁

           if (processLock == null || !processLock.isValid()) {

throw new FileLockedException("download exists: " + saveFilePath);//如果进度锁为空或无效,抛出文件锁异常

           }//等待若不能下载则取消此次下载



           params = request.getParams();//获取请求参数

           {

long range = 0;//初始范围值0

               if (isAutoResume) {

File tempFile = new File(tempSaveFilePath);//如果允许自动恢复,以缓存保存文件路径创建缓存文件

                   long fileLen = tempFile.length();//获取缓存文件长度

                   if (fileLen <= CHECK_SIZE) {

IOUtil.deleteFileOrDir(tempFile);//如果文件长度小于等于检查尺寸512,删除缓存文件

                       range = 0;//范围值设为0

                   } else {

range = fileLen - CHECK_SIZE;//如果文件长度大于检查尺寸512,设置范围值为文件长度减检查尺寸

                   }

}

params.setHeader("RANGE", "bytes=" + range + "-");//请求参数设置头;retry时需要覆盖RANGE参数

           }//处理【断点逻辑1】(见文件头doc)代码块



           if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {

throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新进度失败,抛出已取消异常

           }



request.sendRequest();//发送请求;可能抛出网络异常



           contentLength = request.getContentLength();//Uri请求参数获取内容长度

           if (isAutoRename) {

responseFileName = getResponseFileName(request);//如果允许自动重命名,以Uri请求参数获取响应文件名

           }

if (isAutoResume) {

isAutoResume = isSupportRange(request);//如果允许自动恢复,判断是否支持范围赋值是否自动恢复

           }



if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {

throw new Callback.CancelledException("download stopped!");//如果进度操作器非空且更新进度失败,抛出已取消异常

           }



if (diskCacheFile != null) {

DiskCacheEntity entity = diskCacheFile.getCacheEntity();//如果磁盘缓存文件非空,获取磁盘缓存实体

               entity.setLastAccess(System.currentTimeMillis());//设置最近访问实际为当前时间

               entity.setEtag(request.getETag());//设置电子标签

               entity.setExpires(request.getExpiration());//设置到期

               entity.setLastModify(new Date(request.getLastModified()));//设置最后修改日期

           }

result = this.load(request.getInputStream());//调用本类加载输入流方法获取文件

       } catch (HttpException httpException) {

if (httpException.getCode() == 416) {

if (diskCacheFile != null) {

result = diskCacheFile.commit();//捕获网络异常,如果获取代码为416,并且磁盘缓存文件非空,提交磁盘缓存文件

               } else {

result = new File(tempSaveFilePath);//如果磁盘缓存文件为空,创建缓存保存文件路径

               }

//从缓存获取文件,不rename和断点,直接退出

               if (result != null && result.exists()) {

if (isAutoRename) {

responseFileName = getResponseFileName(request);//如果结果文件非空且存在,并且允许自动重命名,获取响应文件名

                   }

result = autoRename(result);//调用本类中自动重命名方法

               } else {

IOUtil.deleteFileOrDir(result);//否则删除结果文件

                   throw new IllegalStateException("cache file not found" + request.getCacheKey());//抛出非法状态异常

               }

} else {

throw httpException;//抛出网络异常

           }

} finally {

IOUtil.closeQuietly(processLock);//关闭进度锁

           IOUtil.closeQuietly(diskCacheFile);//关闭磁盘缓存文件

       }

return result;

   }



/**

    * 初始化磁盘缓存文件

    * @param request Uri请求参数

    * @throws Throwable 抛出异常

    */

   private void initDiskCacheFile(final UriRequest request) throws Throwable {

DiskCacheEntity entity = new DiskCacheEntity();//创建磁盘缓存实体

       entity.setKey(request.getCacheKey());//从Uri请求参数获取缓存键,为磁盘缓存实体设置键

       diskCacheFile = LruDiskCache.getDiskCache(params.getCacheDirName()).createDiskCacheFile(entity);//以请求参数获取缓存目录名,再以磁盘缓存实体为参数创建磁盘缓存文件,作为参数获取磁盘缓存

       if (diskCacheFile != null) {

saveFilePath = diskCacheFile.getAbsolutePath();//如果磁盘缓存文件非空,获取绝对路径作为保存文件路径

           //磁盘缓存文件是缓存路径,磁盘缓存文件的commit方法返回目标文件

           tempSaveFilePath = saveFilePath;//保存文件路径作为缓存保存文件路径

           isAutoRename = false;//设置不自动重命名

       } else {

throw new IOException("create cache file error:" + request.getCacheKey());//如果磁盘缓存文件为空,抛出输入输出异常

       }

}//保存路径为空,存入磁盘缓存



   /**

    * 自动重命名

    * @param loadedFile 已加载文件

    * @return 返回文件

    */

   private File autoRename(File loadedFile) {

if (isAutoRename && loadedFile.exists() && !TextUtils.isEmpty(responseFileName)) {

File newFile = new File(loadedFile.getParent(), responseFileName);//如果允许自动重命名,且已加载文件存在,且响应文件名非空,以响应文件名新建父路径

           while (newFile.exists()) {

newFile = new File(loadedFile.getParent(), System.currentTimeMillis() + responseFileName);//如果新文件存在,以当前实际加响应文件名创建父路径

           }

return loadedFile.renameTo(newFile) ? newFile : loadedFile;//返回已加载文件重命名为新文件成功,则返回新文件名,否则返回已加载文件名

       } else if (!saveFilePath.equals(tempSaveFilePath)) {

File newFile = new File(saveFilePath);//如果缓存保存文件路径与保存文件路径不同,已保存文件路径创建新文件

           return loadedFile.renameTo(newFile) ? newFile : loadedFile;//返回已加载文件重命名为新文件成功,则返回新文件名,否则返回已加载文件名

       } else {

return loadedFile;//不符合以上条件,直接返回已加载文件

       }

}//处理【下载逻辑2.b】(见文件头doc)



   /**

    * 获取响应文件名

    * @param request Uri请求参数

    * @return 返回响应文件名或null

    */

   private static String getResponseFileName(UriRequest request) {

if (request == null) return null;//如果Uri请求参数为空,返回null

       String disposition = request.getResponseHeader("Content-Disposition");//获取Content-Disposition响应头作为响应字符串

       if (!TextUtils.isEmpty(disposition)) {

int startIndex = disposition.indexOf("filename=");//如果配置字符串非空,获取filename=索引位置作为开始位置

           if (startIndex > 0) {

startIndex += 9;//如果开始位置大于0则增加9:“filename=”.length()

               int endIndex = disposition.indexOf(";", startIndex);//获取跳过开始索引位置后,分号索引位置作为结束位置

               if (endIndex < 0) {

endIndex = disposition.length();//如果结束位置小于0,获取响应字符串长度

               }

if (endIndex > startIndex) {

try {

String name = URLDecoder.decode(

disposition.substring(startIndex, endIndex),

                               request.getParams().getCharset());//如果结束位置小于开始位置,解析子字符串作为名称

                       if (name.startsWith("\"") && name.endsWith("\"")) {

name = name.substring(1, name.length() - 1);//如果名称以引号开始结束,从名称获取去引号子字符串

                       }

return name;//返回名称

                   } catch (UnsupportedEncodingException ex) {

LogUtil.e(ex.getMessage(), ex);//捕获不支持编码异常,记录日志

                   }

}

}

}

return null;

   }



/**

    * 判断是否支持范围

    * @param request Uri请求参数

    * @return 返回是否支持范围

    */

   private static boolean isSupportRange(UriRequest request) {

if (request == null) return false;//如果Uri请求参数为空,返回false

       String ranges = request.getResponseHeader("Accept-Ranges");//获取Accept-Ranges响应头

       if (ranges != null) {

return ranges.contains("bytes");//如果响应头非空,返回是否包含“bytes”字符串

       }

ranges = request.getResponseHeader("Content-Range");//获取获取Accept-Ranges响应头为空,获取Content-Range响应头

       return ranges != null && ranges.contains("bytes");//返回是否响应头非空且包含“bytes”字符串

   }



/**

    * 加载,从磁盘缓存实体

    * @param cacheEntity 磁盘缓存实体

    * @return 返回文件

    * @throws Throwable 抛出异常

    */

   @Override

   public File loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable {

return LruDiskCache.getDiskCache(params.getCacheDirName()).getDiskCacheFile(cacheEntity.getKey());//获取磁盘缓存,再获取磁盘缓存文件

   }



/**

    * 保存到缓存实体

    * @param request Uri请求参数

    */

   @Override

   public void save2Cache(final UriRequest request) {

}//实现空方法,已经通过磁盘缓存文件commit方法保存

}

org.xutils.http.loader.FileLoader