使用 python 模擬 laravel filesystem
This is a tutorial of how to make a laravel’s filesystem-like feature in python
1. A interface
We can create interface by python’s abc
module
import abc
class FileSystemManager(abc.ABC):
_instance: Any = None
def __new__(cls, *args: Any) -> 'FileSystemManager':
# use singleton for saving memory usage
if not cls._instance:
cls._instance = super(FileSystemManager, cls).__new__(cls)
return cls._instance
@classmethod
@abc.abstractmethod
def register(cls) -> 'FileSystemManager':
# register new disk provider
...
@abc.abstractmethod
def write(self, filepath: str) -> None
...
# and other file related methods like read,mkdir,rmdir...
2. A caller like laravel’s Storage Facade
Then we wants to interactive with the contract like laravel’s Storage::disk()
, so make a Storage
class with @classmethod
class Storage:
# provider map, acts like factory
instances = {'local': LocalProvider, 's3': S3Provider}
@classmethod
def disk(cls, driver: str = 'file'):
return cls.instances[driver].register()
3. Implement providers
As you can see at second step, there’s two providers to implement
1. LocalProvider
import os
from . import FileSystemManager
class LocalProvider(FileSystemManager):
def __init__(self) -> None:
self.root = '/path/to/root/storage'
@classmethod
def register(cls) -> 'LocalProvider':
return cls()
def read(self, filepath: str) -> str | None:
return FileUtil.read(f'{self.root}{filepath}')
def write(self, filepath: str, content: Any, **kwargs: str | None) -> None:
if not isinstance(content, str):
raise TypeError('content MUST be a str')
FileUtil.write(f'{self.root}{filepath}', content)
def exists(self, filepath: str) -> bool:
return FileUtil.exists(f'{self.root}{filepath}')
def url(self, filename: str) -> str:
return f'https://{your_host}/public/{filename}'
def make_dirs(self, filepath: str) -> None:
FileUtil.make_dirs(f'{self.root}{filepath}')
def remove_file(self, filepath: str) -> None:
FileUtil.remove_file(filepath)
def remove_dir(self, dir_path: str) -> None:
FileUtil.remove_dir(dir_path)
2. S3Provider
import logging
from . import FileSystemManager
import boto3
from botocore.exceptions import ClientError
class S3Provider(FileSystemManager):
def __init__(self) -> None:
config = {
'id': 'AWS_ACCESS_KEY_ID',
'secret': 'AWS_SECRET_ACCESS_KEY',
'region': 'AWS_S3_REGION',
'bucket': 'AWS_S3_BUCKET'
}
self.config = config
self.s3 = boto3.client('s3',
aws_access_key_id=config['id'],
aws_secret_access_key=config['secret'],
region_name=config['region'])
@classmethod
def register(cls) -> 'S3Provider':
return cls()
def read(self, filepath: str) -> str | None:
s3_obj = self.s3.get_object(Bucket=self.config['bucket'], Key=filepath)
response = s3_obj['Body'].read()
return response.decode('utf-8')
def write(self, filepath: str, content: Any, **kwargs: str | None) -> None:
if isinstance(content, str):
raise TypeError('content MUST be types in BytesIO|StringIO')
self.s3.upload_fileobj(
content,
self.config['bucket'],
filepath,
ExtraArgs={
'ACL': 'public-read',
'ContentType': kwargs.get('content_type', 'application/octet-stream')
})
def exists(self, filepath: str) -> bool:
try:
response = self.s3.head_object(Bucket=self.config['bucket'], Key=filepath)
return response['ResponseMetadata']['HTTPStatusCode'] == 200
except ClientError as e:
logging.info(e)
return False
def url(self, filename: str) -> str:
return f'https://{self.config["bucket"]}.s3.{self.config["region"]}.amazonaws.com/{filename}'
def make_dirs(self, filepath: str) -> None:
raise NotImplementedError(
f'method make_dirs not implemented in {self.__class__}')
def remove_file(self, filepath: str) -> None:
self.s3.delete_object(Bucket=self.config['bucket'], Key=filepath)
def remove_dir(self, dir_path: str) -> None:
if dir_path[-1] != '/':
raise ValueError('dir_path MUST endswith "/"')
response = self.s3.list_objects(Bucket=self.config['bucket'], Prefix=dir_path)
if 'Contents' in response:
for obj in response['Contents']:
self.remove_file(obj['Key'])
4. start use
After implemented all of them, we can call it like laravel filesystem
Storage.disk('s3').read(...)
Storage.disk('local').write(..., ...)