标签归档:文件系统

Laravel FileSystem详解

存储一个文件,需要知道存放在哪个磁盘以及磁盘上的那个位置,还需要设置文件的可见性(private或public),如果是public的,还需要知道怎么访问。

不管是本地磁盘还是网络磁盘,都可以抽象为磁盘,在磁盘上,可做的操作一样,这个就是League\Flysystem包做的事情。而Laravel的文件系统,是对这个包的二次封装(提供一个新的适配器)。

服务提供:
Illuminate\Filesystem\FilesystemServiceProvider

class FilesystemServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->registerNativeFilesystem();

        $this->registerFlysystem();
    }

    protected function registerNativeFilesystem()
    {
        $this->app->singleton('files', function () {
            return new Filesystem;
        });
    }

    protected function registerFlysystem()
    {
        $this->registerManager();

        $this->app->singleton('filesystem.disk', function () {
            return $this->app['filesystem']->disk($this->getDefaultDriver());
        });

        $this->app->singleton('filesystem.cloud', function () {
            return $this->app['filesystem']->disk($this->getCloudDriver());
        });
    }

    protected function registerManager()
    {
        $this->app->singleton('filesystem', function () {
            return new FilesystemManager($this->app);
        });
    }
}

首先注意到这里的registerNativeFilesystem直接是往容器中注册files,指向Illuminate\Filesystem\Filesystem实例。它是Laravel提供的一个本地文件系统封装,只是用来方便操作本地任意的文件或目录。这个对象内部使用了Symfony\Component\Finder\Finder来实现文件的查找操作,同时提供了一致的API,比如get()、put()、exists()、delete()、prepend()、append()等等,特别:

// 先上锁
    public function sharedGet($path)
    {
        $contents = '';

        $handle = fopen($path, 'rb');

        if ($handle) {
            try {
                if (flock($handle, LOCK_SH)) {
                    clearstatcache(true, $path);

                    $contents = fread($handle, $this->size($path) ?: 1);

                    flock($handle, LOCK_UN);
                }
            } finally {
                fclose($handle);
            }
        }

        return $contents;
    }

// 创建文件或目录的符号链接(Windows下如何实现)
    public function link($target, $link)
    {
        if (! windows_os()) {
            return symlink($target, $link);
        }

        $mode = $this->isDirectory($target) ? 'J' : 'H';

        exec("mklink /{$mode} \"{$link}\" \"{$target}\"");
    }

注意,可以通过app(‘files’)获取这个对象,另外它对应了一个叫File的Facade,可以充分使用:

$path = public_path();
$file = $path . '/t.txt';
if (\File::exists($file)) {
    \File::delete($file);
}

回到服务提供者,除了registerNativeFilesystem,还有registerManager,实际是一个文件系统管理器,文件系统可以看做是一个磁盘,可以有很多磁盘(比如本地磁盘,网络磁盘),那就需要一个管理器管理这些磁盘,也方便切换。这里的磁盘又可分为默认磁盘和默认网络磁盘(这两个不冲突),默认磁盘和默认网络磁盘可以一样。对应的配置文件:

return [
    'default' => env('FILESYSTEM_DRIVER', 'local'),

    'cloud' => env('FILESYSTEM_CLOUD', 's3'),

    'disks' => [

         'local' => [
             'driver' => 'local',
             'root' => storage_path('app'),
         ],

        'public' => [
            'driver' => 'local',
            'root' => storage_path('app/public'),
            'url' => env('APP_URL').'/storage',
            'visibility' => 'public',
        ],

        's3' => [
            'driver' => 's3',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION'),
            'bucket' => env('AWS_BUCKET'),
        ],

    ],

];

注意disks数组,Key是磁盘名称,原则上可以随意命名,切换磁盘时用的就是这个名称。里面的driver对于内置的来说,就是固定的,如果是自定义的驱动,对应的就是自定义的名字(需要按照League\Flysystem套路来实现驱动),其它配置是因驱动不同而不同。

这里需要看明白的是local和public磁盘的差别,public磁盘存储路径在/storage/app/public中,可见性是“public”的,访问链接是/public/storage,这里意味着需要把/storage/app/public和/public/storage建立一个符号链接,可以调用Illuminate\Filesystem\Filesystem的link()方法可以完成,Laravel本身提供了一个专门针对这个情况的命令行工具:

php artisan storage:link

// Illuminate\Foundation\Console\StorageLinkCommand
// 如下代码
        $this->laravel->make('files')->link(
            storage_path('app/public'), public_path('storage')
        );

默认存储到本地磁盘中的文件(位置:/storage/app/)不是公开的,但是/storage/app/public是公开的,所有需要做一个符号链接。

如果需要存放到一个公开的文件,那么需要切换到public磁盘,然后保存即可:

$public = \Storage::disk("public");
$public->put('folder', 'file.txt');

Storage对应的就是磁盘管理器(app(“filesystem”))。在Illuminate\Filesystem\FilesystemManager中,disk就是一个Illuminate\Filesystem\FilesystemAdapter的实例,它接收一个League\Flysystem\FilesystemInterface类型的实例(League\Flysystem\Filesystem是实现类),而这个实例又需要一个League\Flysystem\AdapterInterface的实例,配置会传递到这个适配器。最终结果就是Laravel通过一个自己的适配器,把League\Flysystem\FilesystemInterface接口的方法更换了一套API名称(当然还有其它的集成)。

1 自定义文件系统
Laravel的适配器需要一个League\Flysystem\Filesystem实例,而League\Flysystem\Filesystem需要一个League\Flysystem\AdapterInterface适配器实例,所以套路是:

class XxxAdaper implements \League\Flysystem\AdapterInterface
{}

Storage::extend('xxx', function ($app, $config) {
            return new Filesystem(new League\Flysystem\Filesystem(new XxxAdaper($config)));
        });

Storage::extend()第一个参数是驱动名称(不是磁盘名称),然后这个磁盘就可用,这里需要做的就是实现XxxAdaper适配器。

2 启用和配置S3
对于S3, Laravel提供了内置支持,不过还需要引入S3的SDK才能正常工作。

AWS提供的SDK,底层服务使用GuzzleHttp来通信,所以理论上应该提供了针对它的相关的控制选项。 SDK中的Aws\S3\S3Client继承自Aws\AwsClient,查看其构造函数说明:

__construct ( array $args ) 
 
http: (array, default=array(0)) Set to an array of SDK request options to apply to each request (e.g., proxy, verify, etc.).

针对Http控制的仅仅有这一样的说明。也没有点出默认是使用GuzzleHttp。实际上,所有针对GuzzleHttp可用的配置参数,这里都可以传递进来:

'http' => [
                'connect_timeout' => 20,
                'timeout' => 60,
                'verify' => false
            ],

比如这里的超时控制,GuzzleHttp默认是0,意思就不超时,这个在后台进程中,经常因为这个默认值,导致进程僵死。又比如,如果要控制其走代理:

// 控制S3是否使用代理
$http_proxy = env('S3_PROXY', false);
if(!empty($http_proxy)) {
    $proxy = env($http_proxy, false);
    if(!empty($proxy)) {
        $config['disks']['s3']['http']['proxy'] = [
            'http'  => 'tcp://'.$proxy,
            'https' => 'tcp://'.$proxy
        ];
    }
}

2 文件下载
磁盘重新中提供了一个download()方法,可以非常方便地下载文件:

return \Storage::download('files/zVolRIUofhOdx9BDJP04WMGoJJonmLcZfITcXI2s.docx');

3 文件上传
Illuminate\Http\Request使用了Illuminate\Http\Concerns\InteractsWithInput,这个trait包含了与输入相关的方法:

// 上传一个文件
<input type="file" name="file" />

// 上传多个文件
<input type="file" name="file[]" />
<input type="file" name="file[]" />

$request->hasFile("file");
$request->file("file");

服务端,控制前中都可以使用$request->hasFile(“file”)判断是否存在文件,然后通过$request->file(“file”)把文件取回。

public function hasFile($key)
    {
        if (! is_array($files = $this->file($key))) {
            $files = [$files];
        }

        foreach ($files as $file) {
            if ($this->isValidFile($file)) {
                return true;
            }
        }

        return false;
    }

protected function isValidFile($file)
    {
        return $file instanceof SplFileInfo && $file->getPath() !== '';
    }

public function file($key = null, $default = null)
    {
        return data_get($this->allFiles(), $key, $default);
    }

   public function allFiles()
    {
        $files = $this->files->all();

        return $this->convertedFiles
                    ? $this->convertedFiles
                    : $this->convertedFiles = $this->convertUploadedFiles($files);
    }

    protected function convertUploadedFiles(array $files)
    {
        return array_map(function ($file) {
            if (is_null($file) || (is_array($file) && empty(array_filter($file)))) {
                return $file;
            }

            return is_array($file)
                        ? $this->convertUploadedFiles($file)
                        : UploadedFile::createFromBase($file);
        }, $files);
    }

注意,$this->files是所有上传的文件(集合),每个文件都会转换为Illuminate\Http\UploadedFile对象。

PHP中,对文件有三个标准类:

SplFileInfo
SplFileObject
SplTempFileObject

SplTempFileObject继承SplFileObject,SplFileObject继承SplFileInfo,SplFileInfo实现了一个文件的封装,提供了非常常用的方法。

Symfony\Component\HttpFoundation\File\File继承了SplFileInfo,在它之上,提供了guessExtension()、getMimeType()方法。

Symfony\Component\HttpFoundation\File\UploadedFile继承了Symfony\Component\HttpFoundation\File\File,在它基础之上提供了文件上传需要的特定方法,比如:getClientOriginalName()、getClientOriginalExtension()、getClientMimeType()、guessClientExtension()、isValid()、move()等方法,

Illuminate\Http\UploadedFile继承了Symfony\Component\HttpFoundation\File\UploadedFile,提供了与框架相关的保存文件的方法。比如:store()、storePublicly()、storeAs()等。

例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/test/upload" method="post" enctype="multipart/form-data">
    {{ csrf_field() }}
    {{-- 单选 --}}
    <input type="file" name="file" />               
    {{-- 多选,表单名字不带[]--}}
    <input type="file" name="files" multiple />
     {{-- 多选,表单名字带[]--}}
    <input type="file" name="files1[]" multiple />
    {{-- 多选加单选 --}}
    <input type="file" name="files2[]" multiple />
    <input type="file" name="files2[]" />
    
    <br />
    <input type="submit" value="提交" />
</form>
</body>
</html>

// 文件处理
   public function upload(Request $request)
   {
      $files = $request->allFiles();
      print_r($files);
    }

输出:

Array
(
    [file] => Illuminate\Http\UploadedFile Object
        (
            [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
            [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
            [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
            [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
            [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
            [hashName:protected] => 
            [pathName:SplFileInfo:private] => /private/var/tmp/php07Byk0
            [fileName:SplFileInfo:private] => php07Byk0
        )

    [files] => Illuminate\Http\UploadedFile Object
        (
            [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
            [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.png
            [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/png
            [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 17417
            [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
            [hashName:protected] => 
            [pathName:SplFileInfo:private] => /private/var/tmp/phpd9uAtT
            [fileName:SplFileInfo:private] => phpd9uAtT
        )

    [files1] => Array
        (
            [0] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpzJvo3C
                    [fileName:SplFileInfo:private] => phpzJvo3C
                )

            [1] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.png
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/png
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 17417
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpHCVvIK
                    [fileName:SplFileInfo:private] => phpHCVvIK
                )

        )

    [files2] => Array
        (
            [0] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/php50xae1
                    [fileName:SplFileInfo:private] => php50xae1
                )

            [1] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => test.png
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/png
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 17417
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpuHaPFq
                    [fileName:SplFileInfo:private] => phpuHaPFq
                )

            [2] => Illuminate\Http\UploadedFile Object
                (
                    [test:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 
                    [originalName:Symfony\Component\HttpFoundation\File\UploadedFile:private] => a.jpg
                    [mimeType:Symfony\Component\HttpFoundation\File\UploadedFile:private] => image/jpeg
                    [size:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 3305
                    [error:Symfony\Component\HttpFoundation\File\UploadedFile:private] => 0
                    [hashName:protected] => 
                    [pathName:SplFileInfo:private] => /private/var/tmp/phpXU4PAy
                    [fileName:SplFileInfo:private] => phpXU4PAy
                )

        )
)

这个结果非常清晰:
1 对于单选,识别为一个文件对象
2 对于多选,如果表单name不带[],还是识别为一个文件对象,这个文件对象为最后选中的文件
3 对于多选,表单name必须携带[],这样可以识别为一个数组
4 可以混合使用单选和多选,只要表单name相同即可
5 识别的Key不带[]

文件上传到临时目录后,如果正确上传,会通过验证:

    public function isValid()
    {
        $isOk = UPLOAD_ERR_OK === $this->error;

        return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
    }

通过验证后需要保存到一个正常的目录,可以使用move()方法,不过Laravel提供了一套方案,可以方便地保存到磁盘:

public function store($path, $options = [])
    {
        return $this->storeAs($path, $this->hashName(), $this->parseOptions($options));
    }

直接调用store可以保存文件到默认磁盘,如果需要切换磁盘,可以定义$options,比如要保存到s3,可以设置$options = [‘disk’ => ‘s3’],背后的逻辑实际就是:

\Storage::disk("s3")->putFile($path, new UploadFile());

如果要设置可见性,可以调用storePublicly(),实际调用storeAs(), 修正$options而已。所以:

storePublicly($path)
//等同
store($path, $option = ["visibility" => "public"]);

对于本地驱动,visibility是一个模拟,理应保存到/storage/app/public中,实际上和store并没有差异。如果要保存到public中还是要[‘disk’ => ‘pubic’]才行。

Node.js 操作文件系统

> 文件读写
完整读取一个文件时,可用readFile方法或readFileSync:

fs.readFile(filename, [options], callback)

var fs = require('fs');
fs.readFile('./t.txt', function(err, data){
	if(err) console.log("读文件发送错误")
	slse console.log(data);
});

注意,如果没有指定options,那么data就是二进制数据(Buffer对象,可调用toString()方法获取字符串),options设置:

flag		r, r+, w, w+, a, a+
encoding	utf8, ascii, base64

在options参数值中,可使用encoding属性指定使用何种编码格式来读取该文件,过程就是读取文件,然后转换成指定编码后存入Buffer对象,否则就是直接读取文件的原始二进制内容。如果在readFile函数中使用options参数并将encoding属性值指定为某种编码格式,则回调函数中的第二个参数值返回将文件内容根据指定编码格式进行编码后的字符串。

在使用同步方式读取文件时,使用readFileSync方法:

var data=fs.readFileSync(filename, [options])

在完整写入一个文件时,我们可以使用fs模块中的writeFile方法或writeFileSync方法:

fs.writeFile(filename,data,[options],callback)
fs.writeFileSync(filename,data, [options]);

var fs = require('fs');
fs.writeFile('./t.txt', "数据", function(err){
	if(err) console.log("写文件失败");
	else console.log("写文件成功");
});

// 向文件追加数据
var fs = require('fs');
fs.writeFile('./t.txt', "追加数据", {flag:'a'}, function(err){
	if(err) console.log("写文件失败");
	else console.log("写文件成功");
});

// base64读入图片,base64解码数据写入图片
// 读入的data是一个Buffer对象,toString后就是base64字符串
// 然后写文件时,指定base64解码字符串(得到二进制数据)
var fs=require('fs');
fs.readFile('./a.gif','base64',function(err,data){
	fs.writeFile('./b.gif',data.toString(), "base64", function(err){});
});

产生data可以是字符串也可以是一个Buffer对象。options设置:

flat		默认为w
mode		文件权限,默认0666
encoding	指定使用何种编码来写入文件,data是Buffer时被忽略

将一个字符串或一个缓存区中的数据追加到一个文件底部时,我们可以使用fs模块中的appendFile方法或appendFileSync方法。

>从指定位置开始读写文件

fs.open(filename, flags,[mode],callback)
fs.openSync(filename, flags,[mode])

//回调函数第二参数为打开的文件描述符
var fs=require('fs');
fs.open('./t.txt', 'r', function(err,fd) {

});

// 取到文件描述符后,使用read读取
fs.read(fd, buffer, offset, length, position, callback)

> 创建与读取目录

// mode默认为0777
fs.mkdir(path, [mode], callback);

// 读取目录,files是一个数组
var fs = require('fs');
fs.readdir('D:/', function(err, files){
    console.log(files);
});

> 查看文件或目录的信息

// 当查看连接文件时,必须使用lstat
fs.stat(path,callback);
fs.lstat(path, callback);

// stats是一个fs.Stats对象,有一系列方法和属性
// 比如是isFile isDirectory
fs.stat('./t.txt', function(err, stats){

});

在使用open方法或openSync方法打开文件并返回文件描述符后,可以使用fs模块中的fstat方法查询被打开的文件信息。

> 检查文件或目录是否存在

fs.exists(path, function(exists){});

> 获取文件或目录的绝对路径

fs.realpath(path, function(err, resolvedPath){});

> 修改文件访问时间及修改时间

fs.utime(path, atime, mtime, function(err){});
fs.utime(path, new Date(), new Date(), function(err){});

在使用open方法或openSync方法打开文件并返回文件描述符后,可以使用fs模块中的futimes方法修改文件的访问时间或修改时间。

> 修改文件或目录的读写权限

fs.chmod('./t.txt', 06000, function(err){});

> 文件或目录的其它操作

fs.rename(oldPath,newPath,callback)
fs.link(srcpath,dstpath,callback)
fs.unlink(path,callback)
fs.symlink(srcpath,dstpath,[type],callback)
fs.truncate(filename,len,callback)
fs.rmdir(path,callback)
fs.watchFile('./message.txt',function(curr, prev) {})