目录
- 前言
- 一、项目配置
- 1.项目依赖
- 2.权限及其他设置
- 二、Application类全局初始化
- 三、权限检查
- 四、上传文件
- 1.文件选择器获取文件路径
- 2.文件上传代码
- 五、下载文件
- 六、工具类文件
- 1.FileUtil
- 2.HttpDownloader
- 七、XML布局文件
- 八、完整代码
- 结尾
前言
Android 上传和下载文件代码,上传通过调用api接口实现,可携带Cookie,Android Studio项目文件(API Level:30)
一、项目配置
1.项目依赖
implementation "androidx.core:core:1.3.2"
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
implementation "com.github.bumptech.glide:glide:4.8.0"
implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
2.权限及其他设置
权限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REPLACE_EXISTING_PACKAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
provider
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
file_paths文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<external-files-path path="/download" name="download"/>
</resources>
二、Application类全局初始化
首先在项目目录下一个Java类继承Application类,实现是onCreate()方法。这个类可以做APP的全局初始化工作。
public class MyApplication extends Application {
private static String okhttpCookie = "";
@Override
public void onCreate() {
super.onCreate();
}
public static String getOkhttpCookie() {
return okhttpCookie;
}
public static void setOkhttpCookie(String okhttpCookie) {
MyApplication.okhttpCookie = okhttpCookie;
}
}
三、权限检查
首次启动时获取应用的网络及文件读取的权限
private void checkPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
try {
if (flag) {
uploadFile();
} else {
upload_res.setText("请选择文件");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 201);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Log.e("tag", "onRequestPermissionsResult: " + requestCode);
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "用户授权", Toast.LENGTH_SHORT).show();
try {
if (flag) {
uploadFile();
} else {
upload_res.setText("请选择文件");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
Toast.makeText(this, "用户未授权", Toast.LENGTH_SHORT).show();
}
}
四、上传文件
1.文件选择器获取文件路径
public void OpenFile(View view) {
// 指定类型
String[] mimeTypes = {"*/*"};
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
StringBuilder mimeTypesStr = new StringBuilder();
for (String mimeType : mimeTypes) {
mimeTypesStr.append(mimeType).append("|");
}
intent.setType(mimeTypesStr.substring(0, mimeTypesStr.length() - 1));
startActivityForResult(Intent.createChooser(intent, "ChooseFile"), REQUEST_FILE_CODE);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_FILE_CODE && resultCode == RESULT_OK) {
Uri uri = data.getData();
Log.d("uri", "uri:" + uri);
upload_url = FileUtil.getPath(this, uri);
Log.d("upload_url", upload_url);
if (!TextUtils.isEmpty(upload_url)) {
flag = true;
}
}
super.onActivityResult(requestCode, resultCode, data);
checkPermission();
}
2.文件上传代码
private void uploadFile() throws IOException {
if (!upload_url.equals("")) {
upload_file = new File(upload_url);
login();
OkHttpClient upload_okHttpClient = new OkHttpClient.Builder().build();
// 上传为图片类型文件
RequestBody requestBody = RequestBody.create(MediaType.parse("image/*"), upload_file);
MultipartBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", upload_file.getName(), requestBody)
.addFormDataPart("folder", "")
.build();
String cookie = MyApplication.getOkhttpCookie();
Request request;
if (cookie != null) {
request = new Request.Builder()
.addHeader("cookie", cookie)
.url("http://192.168.204.68/chfs/upload")
.post(body)
.build();
} else {
request = new Request.Builder()
.url("http://192.168.204.68/chfs/upload")
.post(body)
.build();
}
Call call = upload_okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("upload failure", "onFailure: " + e.getMessage());
}
@SuppressLint("SetTextI18n")
@Override
public void onResponse(Call call, Response response) throws IOException {
upload_url = "";
flag = false;
String json_str = response.body().string();
if (TextUtils.isEmpty(json_str)) {
upload_res.setText(upload_file.getName() + " 上传成功");
} else {
Log.e("UPLOAD_ERROR", json_str);
upload_res.setText(upload_file.getName() + " 上传错误");
}
}
});
} else {
upload_res.setText("请选择文件");
}
}
五、下载文件
文件下载考虑到文件的大小,下载速度等方面的原因,建议使用线程进行文件下载,同时可以在下载前要判断本地是否存在该文件并对其进行相对应的处理
download_button.setOnClickListener(v -> new downloadFileThread().start());
class downloadFileThread extends Thread {
@SuppressLint("SetTextI18n")
public void run() {
HttpDownloader httpDownloader = new HttpDownloader();
int downloadResult = httpDownloader.downloadFiles( "http://192.168.204.68/chfs/shared/" + download_name.trim(), "upload_download", download_name.trim(), getBaseContext());
Log.d("download_result", String.valueOf(downloadResult));
switch (downloadResult) {
case -1:
Log.e("DOWNLOAD_ERROR ", download_name + " download failure");
upload_res.setText(download_name + " 下载失败");
break;
case 0:
Log.d("DOWNLOAD_SUCCESS ", download_name + "download success");
upload_res.setText(download_name + " 下载成功");
break;
default:
Log.e("DOWNLOAD_ERROR ", download_name + " existed");
upload_res.setText(download_name + " 已存在");
break;
}
}
}
六、工具类文件
1.FileUtil
在Android 4.4之后,不能随便在sd卡上面创建文件或者文件夹了,只能在Android/data/你的包名/这个路径下创建或者修改.另外调用getExternalFilesDir(null)方法系统会默认给你创建。
/**
* 文件工具类
*/
public class FileUtil {
private final String SDCardRoot;
/**
* 专为Android4.4设计的从Uri获取文件绝对路径,以前的方法已不好使
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
//一些三方的文件浏览器会进入到这个方法中,例如ES
//QQ文件管理器不在此列
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {// MediaStore (and general)
// Return the remote address
if (isGooglePhotosUri(uri))
return uri.getLastPathSegment();
if (isQQMediaDocument(uri)) {
String path = uri.getPath();
File fileDir = Environment.getExternalStorageDirectory();
File file = new File(fileDir, path.substring("/QQBrowser".length(), path.length()));
return file.exists() ? file.toString() : null;
}
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {// File
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = MediaStore.MediaColumns.DATA;
final String[] projection = {column};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
/**
* 使用第三方qq文件管理器打开
*
* @param uri
* @return
*/
public static boolean isQQMediaDocument(Uri uri) {
return "com.tencent.mtt.fileprovider".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is Google Photos.
*/
public static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
public FileUtil(Context context) {
//得到当前外部存储设备的目录
SDCardRoot = context.getExternalFilesDir(null).getAbsolutePath() + File.separator;
//File.separator为文件分隔符”/“,方便之后在目录下创建文件
}
/**
* 在SD卡上创建文件
*
*
* @param fileName, dir
* @return file
*/
public File createFileInSDCard(String fileName, String dir) throws IOException {
File file = new File(SDCardRoot + dir + File.separator + fileName);
file.createNewFile();
return file;
}
/**
* 在SD卡上创建目录
*
*
* @param dir
* @return file
*/
public void createSDDir(String dir) throws IOException {
File dirFile = new File(SDCardRoot + dir);
if (!dirFile.exists()) {
if (dirFile.mkdirs()) {
Log.d("SUCCESS", "The directory was created successfully");
}
} else {
Log.d("SUCCESS", "Directory already exists");
}
}
/**
* 判断文件是否存在
*
*
* @param fileName, dir
* @return Does the file exist
*/
public boolean isFileExist(String fileName, String dir) throws IOException {
File file = new File(SDCardRoot + dir + File.separator + fileName);
return file.exists();
}
/**
* 将一个InoutStream里面的数据写入到SD卡中
*
*
* @param fileName, dir, input
* @return Write file
*/
public File write2SDFromInput(String fileName, String dir, InputStream input) {
File file = null;
OutputStream output = null;
try {
//创建目录
createSDDir(dir);
//创建文件
file = createFileInSDCard(fileName, dir);
//写数据流
output = new FileOutputStream(file);
byte buffer[] = new byte[4 * 1024];//每次存4K
int temp;
//写入数据
while ((temp = input.read(buffer)) != -1) {
output.write(buffer, 0, temp);
}
output.flush();
} catch (Exception e) {
System.out.println("写数据异常:" + e);
} finally {
try {
output.close();
} catch (Exception e2) {
System.out.println(e2);
}
}
return file;
}
}
2.HttpDownloader
/**
* 下载工具类
*/
public class HttpDownloader {
String line = null;
StringBuffer strBuffer = new StringBuffer();
BufferedReader bufferReader = null;
//可以下载任意文件,例如MP3,并把文件存储在制定目录(-1:下载失败,0:下载成功,1:文件已存在)
public int downloadFiles(String urlStr, String path, String fileName, Context context) {
try {
FileUtil fileUtil = new FileUtil(context);
fileUtil.createSDDir(path);
if (fileUtil.isFileExist(fileName, path)) {
return 1;
} else {
//判断文件是否存在
InputStream inputStream = getInputStreamFromUrl(urlStr);
File resultFile = fileUtil.write2SDFromInput(fileName, path, inputStream);
if (resultFile == null) return -1;
}
} catch (Exception e) {
System.out.println("读写数据异常:");
return -1;
}
return 0;
}
public InputStream getInputStreamFromUrl(String urlStr) throws IOException {
//创建一个URL对象
URL url = new URL(urlStr);
//创建一个HTTP链接
HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
//使用IO流获取数据
InputStream inputStream = urlConn.getInputStream();
return inputStream;
}
}
七、XML布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/upload"
android:layout_width="match_parent"
android:layout_height="50dp">
<TextView
android:id="@+id/upload_title"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:text="@string/upload"
android:textStyle="bold"
android:textSize="16sp"
android:padding="10dp"/>
</RelativeLayout>
<Button
android:id="@+id/upload_button"
android:layout_width="110dp"
android:layout_height="50dp"
android:layout_marginTop="60dp"
android:layout_below="@id/upload"
android:layout_centerHorizontal="true"
android:gravity="left|center"
android:text="@string/upload"
android:textSize="10sp"
android:textStyle="bold"
android:background="@drawable/upload_button"
android:drawableStart="@mipmap/upload"
android:drawableLeft="@mipmap/upload"
android:drawablePadding="5dp"
android:paddingStart="10dp"
android:paddingLeft="10dp"
android:shadowColor="@color/grey"
tools:ignore="RtlHardcoded,RtlSymmetry,SmallSp" />
<Button
android:id="@+id/download_button"
android:layout_width="110dp"
android:layout_height="50dp"
android:layout_marginTop="20dp"
android:layout_below="@id/upload_button"
android:layout_centerHorizontal="true"
android:gravity="left|center"
android:text="@string/download"
android:textSize="10sp"
android:textStyle="bold"
android:background="@drawable/download_button"
android:drawableStart="@mipmap/download"
android:drawableLeft="@mipmap/download"
android:drawablePadding="5dp"
android:paddingStart="10dp"
android:paddingLeft="10dp"
android:shadowColor="@color/grey"
tools:ignore="RtlHardcoded,RtlSymmetry,SmallSp" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/download_button"
android:gravity="center_horizontal"
android:layout_marginTop="10dp">
<TextView
android:id="@+id/upload_res"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:padding="10dp"
android:gravity="center"
android:text="@string/upload_res"/>
</RelativeLayout>
</RelativeLayout>
八、完整代码
public class MainActivity extends AppCompatActivity {
private Button upload_btn;
private Button download_button;
private TextView upload_res;
private File upload_file;
private String upload_url;
private String download_name = "";
private final int REQUEST_FILE_CODE = 1000;
boolean flag = false;
private void init() {
upload_btn = findViewById(R.id.upload_button);
upload_res = findViewById(R.id.upload_res);
download_button = findViewById(R.id.download_button);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
// 设置监听
upload_btn.setOnClickListener(this::OpenFile);
download_button.setOnClickListener(v -> new downloadFileThread().start());
}
private void checkPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
try {
if (flag) {
uploadFile();
} else {
upload_res.setText("请选择文件");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 201);
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Log.e("tag", "onRequestPermissionsResult: " + requestCode);
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "用户授权", Toast.LENGTH_SHORT).show();
try {
if (flag) {
uploadFile();
} else {
upload_res.setText("请选择文件");
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
Toast.makeText(this, "用户未授权", Toast.LENGTH_SHORT).show();
}
}
private void login() throws IOException {
OkHttpClient login_okHttpClient = new OkHttpClient.Builder().build();
FormBody.Builder builder = new FormBody.Builder();
Map<String, String> params = new HashMap<>();
params.put("user", "admin");
params.put("pwd", "admin");
Set<String> key_set = params.keySet();
for (String key : key_set) {
builder.add(key, Objects.requireNonNull(params.get(key)));
}
FormBody formBody = builder.build();
String login_url = "http://192.168.204.68/chfs/session";
Request login_request = new Request.Builder().url(login_url).post(formBody).build();
Call call = login_okHttpClient.newCall(login_request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("ERROR", "onFailure: " + e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response != null) {
Headers headers = response.headers();
List<String> Cookies = headers.values("Set-Cookie");
if (Cookies.size() > 0) {
String s = Cookies.get(0);
if (!TextUtils.isEmpty(s)) {
int size = s.length();
int i = s.indexOf(";");
if (i < size && i >= 0) {
String res = s.substring(0, i);
MyApplication.setOkhttpCookie(res);
}
}
}
Log.d("SUCCESS", "get Cookie");
Log.d("SUCCESS", MyApplication.getOkhttpCookie());
} else {
IOException IOExceptionx = new IOException();
Log.e("ERROR", IOExceptionx.getMessage());
}
}
});
}
private void uploadFile() throws IOException {
if (!upload_url.equals("")) {
upload_file = new File(upload_url);
login();
OkHttpClient upload_okHttpClient = new OkHttpClient.Builder().build();
RequestBody requestBody = RequestBody.create(MediaType.parse("image/*"), upload_file);
MultipartBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", upload_file.getName(), requestBody)
.addFormDataPart("folder", "")
.build();
String cookie = MyApplication.getOkhttpCookie();
Request request;
if (cookie != null) {
request = new Request.Builder()
.addHeader("cookie", cookie)
.url("http://192.168.204.68/chfs/upload")
.post(body)
.build();
} else {
request = new Request.Builder()
.url("http://192.168.204.68/chfs/upload")
.post(body)
.build();
}
Call call = upload_okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e("upload failure", "onFailure: " + e.getMessage());
}
@SuppressLint("SetTextI18n")
@Override
public void onResponse(Call call, Response response) throws IOException {
upload_url = "";
flag = false;
String json_str = response.body().string();
if (TextUtils.isEmpty(json_str)) {
upload_res.setText(upload_file.getName() + " 上传成功");
} else {
Log.e("UPLOAD_ERROR", json_str);
upload_res.setText(upload_file.getName() + " 上传错误");
}
}
});
} else {
upload_res.setText("请选择文件");
}
}
public void OpenFile(View view) {
// 指定类型
String[] mimeTypes = {"*/*"};
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
StringBuilder mimeTypesStr = new StringBuilder();
for (String mimeType : mimeTypes) {
mimeTypesStr.append(mimeType).append("|");
}
intent.setType(mimeTypesStr.substring(0, mimeTypesStr.length() - 1));
startActivityForResult(Intent.createChooser(intent, "ChooseFile"), REQUEST_FILE_CODE);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_FILE_CODE && resultCode == RESULT_OK) {
Uri uri = data.getData();
Log.d("uri", "uri:" + uri);
upload_url = FileUtil.getPath(this, uri);
Log.d("upload_url", upload_url);
if (!TextUtils.isEmpty(upload_url)) {
flag = true;
}
}
super.onActivityResult(requestCode, resultCode, data);
checkPermission();
}
class downloadFileThread extends Thread {
@SuppressLint("SetTextI18n")
public void run() {
HttpDownloader httpDownloader = new HttpDownloader();
int downloadResult = httpDownloader.downloadFiles( "http://192.168.204.68/chfs/shared/" + download_name.trim(), "upload_download", download_name.trim(), getBaseContext());
Log.d("download_result", String.valueOf(downloadResult));
switch (downloadResult) {
case -1:
Log.e("DOWNLOAD_ERROR ", download_name + " download failure");
upload_res.setText(download_name + " 下载失败");
break;
case 0:
Log.d("DOWNLOAD_SUCCESS ", download_name + "download success");
upload_res.setText(download_name + " 下载成功");
break;
default:
Log.e("DOWNLOAD_ERROR ", download_name + " existed");
upload_res.setText(download_name + " 已存在");
break;
}
}
}
}
结尾
如果没有相应的API接口,可以使用CuteHttpFileServer/chfs(CuteHttpFileServer/chfs是一个免费的、HTTP协议的文件共享服务器,使用浏览器可以快速访问。)作为替代,这是官方网址:http://iscute.cn/chfs。如果觉得可以,麻烦点个赞吧。