Flask,VueJS 와 RethinkDB  를 사용하여 파일 저장 서비스 구축하기 - (1)

[원문] https://www.pluralsight.com/guides/python/build-a-simple-file-storage-service-using-vuejs-flask-and-rethinkdb?utm_campaign=RethinkDB%2BWeekly&utm_medium=web&utm_source=RethinkDB_Weekly_8


소개

간단한 파일 저장 서비스를 작성하는 방법에 대해 소개 해 보려 합니다. 프런트 엔드를 처리하기 위해서는 VueJS를 사용할 것이고, 백엔드를 위해서는 Flask, 데이터 저장을 위해 RethinkDB를 사용할 것입니다. 진행 하는 도중에 여러 기능들에 대해 계속 소개 할 것 이므로 참고하시구요.

이 연재의 첫 번째 파트에서는 응용 프로그램의 백엔드 구축에 중점을 둘 것입니다.


우리가 만들려는 API 는 무엇? 


 다음과 같은  API를 만들 것 입니다. 우리가 만들려는 파일 저장 서비스를 사용하여 사용자는 다음과 같은 기능을  수행 할 수 있게 됩니다.

  1. 계정 만들기

  2. 로그인

  3. 폴더 및 하위 폴더 만들기 및 관리

  4. 파일을 폴더에 업로드

  5. 파일 속성보기

  6. 파일 편집 및 삭제

API의 경우 다음과 같은 엔드포인트로 이루어져 있습니다. POST,GET,PUT,DELETE 으로 구분 되었습니다.

  • POST /api/v1/auth/login - 사용자의 로그인에 사용됨.
  • POST /api/v1/auth/register - 사용자를 등록하기 위해 사용됨
  • GET /api/v1/user/<user_id>/files/ - user_id 를 가진 사용자에 대한 파일들을 리스팅하기 위해  사용됩니다. 
  • POST /api/v1/user/<user_id>/files/ - user_id 를 가진 사용자에 대한 새로운 파일들을 만들 때 사용됩니다.
  • GET /api/v1/user/<user_id>/files/<file_id> - file_id 에 해당하는 파일을 얻기 위해 사용됩니다.
  • PUT /api/v1/user/<user_id>/files/<file_id> - file_id 에 해당하는 파일을 편집하기 위해 사용됩니다.
  • DELETE /api/v1/user/<user_id>/files/<file_id> - file_id 에 해당하는 파일을 삭제하기 위해 사용됩니다.

  • 이제 실제 프로젝트를 위한 과정에 대해 알아 봅시다.


    Setup 하기

    먼저 프로젝트를 위한 디렉토리를 구성 해 봅시다. 아래는 Flask 웹어플리케이션에서 권장되는 구조입니다.

    -- /api
        -- /controllers
        -- /utils
        -- models.py
        -- __init__.py
    -- /templates
    -- /static
        -- /lib
        -- /js
        -- /css
        -- /img
    -- index.html
    -- config.py
    -- run.py
    

    API와 관련 한모듈과 패키지들의 경우 /api 디렉토리에 저장되며, 아래로는 모델을 처리하는 models.py 모듈이 있고 라우팅 목적으로 대부분 사용되는 컨트롤러는 /controllers 패키지에 모듈로 저장됩니다. 


    /api/__init__.py 에는 REST API 를 위한 라우트 및 앱 생성 함수가 추가됩니다. 이 파일 안의 
    create_app () 함수를 통하여 다른 구성으로 여러 개의 app 인스턴스를 만들 수 있습니다. (유연성 증대) 이는 응용 프로그램에 대한 테스트를 작성 할 때 특히 유용합니다.

    자! __init__.py  를 보시죠.

    from flask import Flask, Blueprint
    from flask_restful import Api
    from config import config  
    
    def create_app(env):
        app = Flask(__name__)
        app.config.from_object(config[env])
    
        api_bp = Blueprint('api', __name__)
        api = Api(api_bp)
    
        # Code for adding Flask RESTful resources goes here
    
        app.register_blueprint(api_bp, url_prefix="/api/v1")
    
        return app
    

    여기에서는 env 매개 변수를 사용하는 create_app () 함수를 만들었습니다. 실질적으로 env는 개발, 프로덕션 및 테스트 중 하나 여야 합니다. 즉 env에 제공된 값을 통하여 특정 구성을 로드합니다. 이 구성 정보는 config.py 안에서사전 (key,value) 구조로 저장 되 있습니다.


    이어서 config.py 를 보시죠.

    class Config(object):
        DEBUG = True
        TESTING = False
        DATABASE_NAME = "papers"
    
    class DevelopmentConfig(Config):
        SECRET_KEY = "S0m3S3cr3tK3y"
    
    config = {
        'development': DevelopmentConfig,
        'testing': DevelopmentConfig,
        'production': DevelopmentConfig
    }
    

    현재는 Flask에 디버그 모드로 실행 할 지 여부를 알려주는 DEBUG 매개 변수와 같은 몇 가지 구성 매개 변수 만 지정했습니다.  추가적으로는 모델에서 참조 해야할 DATABASE_NAME 매개 변수와 JWT 토큰 생성을위한 SECRET_KEY 매개 변수도 지정했구요. 현재는 모든 환경에서 동일한 설정을 사용하고 있게 됩니다.

    __init__.py 를 보면, 사용자에게 영향을 주지 않고 핵심 기능의 구현을 변경해야 한다고 생각 한 경우에는 Flask Blueprint를 사용하여 API를 버전화하고 Flask-RESTful API 객체인 api를 초기화 합니다. 잠시 후 이 객체를 사용하여 새 API 엔드포인트를 앱에 추가하는 방법을 보여 드릴 것 입니다.


    다음으로 서버를 실행하기 위한 코드를 루트 디렉토리의 run.py에 넣습니다. Flask-Script를 사용하여 응용 프로그램에 대한 추가 CLI 명령을 만듭니다.

    from flask_script import Manager
    from api import create_app
    
    app = create_app('development')
    
    manager = Manager(app)
    
    @manager.command
    def migrate():
        # Migration script
        pass
    
    if __name__ == '__main__':
        manager.run()

    여기서는 flask_script.Manager 클래스를 사용하여 서버 실행을 추상화하고 CLI에 대한 새 명령을 추가 할 수 있게 했습니다. migrate()는 우리 모델에 필요한 모든 테이블을 자동으로 생성하는 데 사용 될 수 있습니다. 이것은 현재 연습을 위한 단순화 된 해결책이며,  여기까지 모두 정상 상태라면 오류가 없어야 합니다.

    이제 커맨드라인에서 대망의 ~~!!!!

    python run.py runserver 

    기본 포트 5000으로 서버를 실행 해 보시길 바랍니다.


    사용자 모델 

    다음은 모델에 대한 코드를 추가해 보겠습니다. 이 애플리케이션의 경우 두 가지 모델이 필요하지만 튜토리얼의 이 단계에서는 사용자 모델만 작성합니다. 먼저 RethinkDB 데이터베이스에 대한 연결 개체를 만듭니다.

    import rethinkdb as r
    
    from flask import current_app
    
    conn = r.connect(db="papers")
    
    class RethinkDBModel(object):
        pass

    어플리케이션 config 에서 데이터베이스 이름을 참조했습니다. 플라스크에는 현재 실행중인 응용 프로그램 인스턴스에 대한 참조를 보유하는 current_app 변수가 있습니다.

    빈 RethinkDBModel 클래스를 작성한 이유가 무엇이냐고 묻는다면.. 음, 모델 클래스를 통해 공유하고 싶은 몇 가지 사항이 있을 수 있습니다. 이 클래스는 크로스 모델 공유가 필요한 경우를 위한 것입니다.

    우리의 User 클래스는 이 빈 기본 클래스에서 상속 받아서 만들어질 예정이며 User 에는 컨트롤러에서 데이터베이스와 상호 작용하는 데 사용할 수 있는 몇 가지 기능이 들어 있게 됩니다.

    일단 create () 함수로 시작해 봅니다. 이 함수는 DB테이블에 사용자 문서를 생성해야 할 때 호출됩니다.

    class User(RethinkDBModel):
        _table = 'users'
    
        @classmethod
        def create(cls, **kwargs):
            fullname = kwargs.get('fullname')
            email = kwargs.get('email')
            password = kwargs.get('password')
            password_conf = kwargs.get('password_conf')
            if password != password_conf:
                raise ValidationError("Password and Confirm password need to be the same value")
            password = cls.hash_password(password)
            doc = {
                'fullname': fullname,
                'email': email,
                'password': password,
                'date_created': datetime.now(r.make_timezone('+01:00')),
                'date_modified': datetime.now(r.make_timezone('+01:00'))
            }
            r.table(cls._table).insert(doc).run(conn)
    

    여기에서는 classmethod 데코레이터를 사용하고 있습니다. 이 데코레이터를 사용하면 메소드 본문 내에서 클래스 인스턴스에 액세스 할 수 있습니다. (역주: 파이썬에서는 this 를 명시적으로 self 라고 말하며, self 는 객체를 지칭하는 것이라면 여기서의 cls 는 해당 클래스를 지칭합니다. 참고로 파이썬에서 classmethod 와 staticmethod 는 다르며 단순하게는 상속구조가 있다면 classmethod 를 사용하시고, 유틸리티성이라면 staticmethod 를 사용하시면 됩니다.) 클래스 인스턴스를 사용하여 메서드 내에서 _table 속성에 액세스합니다. _table은 해당 모델의 테이블 이름을 저장합니다.

    또한 여기에는 password 및 password_conf 필드가 동일한 지 확인하는 코드가 추가 되었습니다. 일치 하지 않는 경우 ValidationError가 발생하며 예외는 /api/utils/errors.py 모듈에 저장됩니다.

    class ValidationError(Exception):
        pass
    

    디버깅 하기 쉽게 하기 위해 이름 붙혀진 예외를 사용하고 있구요.

    datetime.now (r.make_timezone( '+ 01:00')) 를 어떻게 사용했는지 잘 보세요. 시간대 없이 datetime.now()를 사용할 때 직면 할 문제가 있는데요. RethinkDB 에서는 문서의 날짜 필드에 표준 시간대 정보를 설정해야합니다. 파이썬 함수는 now() 함수에 대한 매개 변수로 지정하지 않으면 그것을 기본적으로 우리에게 제공하지 않습니다. (역주: datetime.now() 는 현재 타임존에 기반한 값을 리턴함. utcnow() 로 호출하면 UTC 기반, 여기서 우리나라의 경우 +09를 해야할거 같은 느낌이....? 그냥 utcnow 사용하면 될거 같은데 -.-a)  

    r.make_timezone ( '+ 01:00')을 사용하여 datetime.now () 함수에 사용할 수 있는 timezone 객체를 만들 수 있습니다. 모든 것이 잘되고 예외가 발생하지 않으면, r.table (table_name)이 리턴하는 테이블 오브젝트에 대해 insert () 메소드를 호출 합니다. 이 메소드는 데이터가 들어있는 사전 데이터를 받으며, 선택된 테이블에 새 문서로 저장 될 것입니다.

    클래스 메소드인 hash_password () 를 호출했구요. 이 방법은 passlib 패키지의 hash.pbkdf2_sha256 모듈을 사용하여 암호를 안전하게 해싱하여 사용합니다. 그 외에도 암호를 확인하는 방법을 만들어야 합니다.

    from passlib.hash import pbkdf2_sha256
    
    class User(RethinkDBModel):
        _table = 'users'
    
        @classmethod
        def create(cls, **kwargs):
            fullname = kwargs.get('fullname')
            email = kwargs.get('email')
            password = kwargs.get('password')
            password_conf = kwargs.get('password_conf')
            if password != password_conf:
                raise ValidationError("Password and Confirm password need to be the same value")
            password = cls.hash_password(password)
            doc = {
                'fullname': fullname,
                'email': email,
                'password': password,
                'date_created': datetime.now(r.make_timezone('+01:00')),
                'date_modified': datetime.now(r.make_timezone('+01:00'))
            }
            r.table(cls._table).insert(doc).run(conn)
    
        @staticmethod
        def hash_password(password):
            return pbkdf2_sha256.encrypt(password, rounds=200000, salt_size=16)
    
        @staticmethod
        def verify_password(password, _hash):
            return pbkdf2_sha256.verify(password, _hash)

    pbkdf2_sha256.encrypt () 메소드는 rounds 와salt_size 를 적용하여 패스워드를 해싱합니다. 라이브러리 작동 방법에 대한 자세한 내용은 여기 here 를 참조하십시오. PBKDF2를 사용하기로 결정한 것은 아래와 같습니다.

    Security-wise, PBKDF2 is currently one of the leading key derivation functions, and has no known security issues.

    -- Quotes from the passlib documentation

    verify_password 메소드는 암호 문자열과 해시를 사용하여 호출됩니다. 암호가 유효하면 true 또는 false를 리턴합니다.

    이제 validate () 함수로 이동합니다. 이 함수는 메일 주소와 암호를 사용하여 로그인 메서드에서 호출됩니다. 이 함수는 메일 필드를 인덱스로 사용하여 문서가 존재하는지 확인 한 다음 제출된 암호와 DB에 존재 하는 암호를 비교합니다. 또한 토큰 기반 인증을 위해 JWT (JSON Web Token)를 사용 하며, 사용자가 유효한 정보를 제공할 경우 토큰을 생성하게 됩니다.

    아래 정도면 models.py에 대한 로직 추가는 어느 정도 완료 되었다고 볼 수 있겠네요.

    import os
    import rethinkdb as r
    from jose import jwt
    from datetime import datetime
    from passlib.hash import pbkdf2_sha256
    
    from flask import current_app
    
    from api.utils.errors import ValidationError
    
    conn = r.connect(db="papers")
    
    class RethinkDBModel(object):
        pass
    
    
    class User(RethinkDBModel):
        _table = 'users'
    
        @classmethod
        def create(cls, **kwargs):
            fullname = kwargs.get('fullname')
            email = kwargs.get('email')
            password = kwargs.get('password')
            password_conf = kwargs.get('password_conf')
            if password != password_conf:
                raise ValidationError("Password and Confirm password need to be the same value")
            password = cls.hash_password(password)
            doc = {
                'fullname': fullname,
                'email': email,
                'password': password,
                'date_created': datetime.now(r.make_timezone('+01:00')),
                'date_modified': datetime.now(r.make_timezone('+01:00'))
            }
            r.table(cls._table).insert(doc).run(conn)
    
        @classmethod
        def validate(cls, email, password):
            docs = list(r.table(cls._table).filter({'email': email}).run(conn))
    
            if not len(docs):
                raise ValidationError("Could not find the e-mail address you specified")
    
            _hash = docs[0]['password']
    
            if cls.verify_password(password, _hash):
                try:
                    token = jwt.encode({'id': docs[0]['id']}, current_app.config['SECRET_KEY'], algorithm='HS256')
                    return token
                except JWTError:
                    raise ValidationError("There was a problem while trying to create a JWT token.")
            else:
                raise ValidationError("The password you inputted was incorrect.")
    
        @staticmethod
        def hash_password(password):
            return pbkdf2_sha256.encrypt(password, rounds=200000, salt_size=16)
    
        @staticmethod
        def verify_password(password, _hash):
            return pbkdf2_sha256.verify(password, _hash)
    

    validate () 메서드에서 주의해야 할 몇 가지~

    먼저 filter() 함수가 테이블 객체에서 사용되었습니다. 이 명령은 테이블을 검색하는 데 사전을 사용합니다. 이 함수는 경우에 따라 술어(역주: predicate 로 보통 TRUE, FALSE 를 리턴 해주는 함수) 를 사용할 수도 있습니다. 이 술어는 람다 함수가 될 수 있으며 파이썬 필터 함수 또는 인수로 함수를 사용하는 다른 함수와 비슷하게 모양새가 됩니다. 이 함수는 쿼리에서 반환 한 모든 문서에 액세스하는 데 사용할 수 있는 커서를 반환하며 커서는 반복 가능하므로 for ... in 루프를 사용하여 커서 객체를 반복 할 수 있습니다. 이 경우, 우리는 반복 가능한 객체를 파이썬 리스트 함수를 사용하여 리스트로 변환하도록 선택했습니다.

    예상대로, 기본적으로 두 가지 작업을 수행합니다. 메일 주소가 존재하고 있음을 확인 한후에 해당 암호가 정확한지를 확인하는거죠. 첫 번째 부분에서는 기본적으로 컬렉션을 계산하는데  이 값이 비어 있으면 오류가 발생합니다. 두 번째 부분에서는 verify_password () 함수를 호출하여 데이터베이스에서 해시와 함께 제공된 비밀번호를 비교합니다. 일치하지 않으면 예외가 발생합니다.

    또한 jwt.encode()를 사용하여 JWT 토큰을 만들고 이를 컨트롤러에 반환하는 방법도 확인 해 주세요. 이 방법은 매우 간단하며 여기 here 설명서를 볼 수 있습니다. 

    컨트롤러로 넘어 갑시다. 우리는 이 모델에서 뚱뚱한 모델과 슬림한 컨트롤러를 갖는 원칙에 따르려고 했습니다. 대부분의 논리는 모델에 있기 때문입니다. 이렇게 되면 컨트롤러는 API 최종 사용자에게 라우팅 및 오류보고에만 초점을 맞출 수 있게 됩니다.


    인증 컨트롤러 

    인증 컨트롤러의 경우 Flask RESTful 리소스의 하위 클래스를 추가해야 합니다. 단순히 flask_restful.Resource 클래스의 하위 클래스로 만들어 지게 됩니다. 하위 클래스에는 해당 HTTP 요청에 매핑되는 메서드가 자리 잡게 됩니다. 예를 들어 GET 액션을 구현하고자 한다면 Resource 서브 클래스에 get()메소드를 생성 하는 거죠. url 과 이것에 대한 매핑은 api.add_resource() 메소드를 통해 구축되며 조금 있다 살펴 볼 예정입니다.

    간단히 두 가지 클래스를 추가해 보겠습니다. 하나는 login에 대한 POST 작업을 하고, 하나는 register 에 관한 것입니다. 이것들은 /api/controllers/auth.py 파일에 작성 됩니다.

    from flask_restful import Resource
    
    class AuthLogin(Resource):
        def post(self):
            pass
    
    class AuthRegister(Resource):
        def post(self):
            pass
    


    다음으로 /api/__init__.py 파일에서 해당 경로를 만들고 해당 클래스를 참조합니다.

    from flask import Flask, Blueprint
    from flask_restful import Api
    
    from api.controllers import auth
    from config import config
    
    def create_app(env):
        app = Flask(__name__)
        app.config.from_object(config[env])
    
        api_bp = Blueprint('api', __name__)
        api = Api(api_bp)
    
        api.add_resource(auth.AuthLogin, '/auth/login')
        api.add_resource(auth.AuthRegister, '/auth/register')
    
        app.register_blueprint(api_bp, url_prefix="/api/v1")
    
        return app


    로그인에 관련된 중요 컨트롤러 로직을 추가하기 위해 /api/controllers/auth.py 파일로 다시 돌아 가 봅시다.
    여기에 필요한 로직은
     일반적으로 인증 시스템에서 수행하는 것과 유사합니다.

    from flask_restful import reqparse, abort, Resource
    
    from api.models import User
    from api.utils.errors import ValidationError
    
    class AuthLogin(Resource):
        def post(self):
            parser = reqparse.RequestParser()
            parser.add_argument('email', type=str, help='You need to enter your e-mail address', required=True)
            parser.add_argument('password', type=str, help='You need to enter your password', required=True)
    
            args = parser.parse_args()
    
            email = args.get('email')
            password = args.get('password')
    
            try:
                token = User.validate(email, password)
                return {'token': token}
            except ValidationError as e:
                abort(400, message='There was an error while trying to log you in -> {}'.format(e.message))
    
    class AuthRegister(Resource):
        def post(self):
            parser = reqparse.RequestParser()
            parser.add_argument('fullname', type=str, help='You need to enter your full name', required=True)
            parser.add_argument('email', type=str, help='You need to enter your e-mail address', required=True)
            parser.add_argument('password', type=str, help='You need to enter your chosen password', required=True)
            parser.add_argument('password_conf', type=str, help='You need to enter the confirm password field', required=True)
    
            args = parser.parse_args()
    
            email = args.get('email')
            password = args.get('password')
            password_conf = args.get('password_conf')
            fullname = args.get('fullname')
    
            try:
                User.create(
                    email=email,
                    password=password,
                    password_conf=password_conf,
                    fullname=fullname
                )
                return {'message': 'Successfully created your account.'}
            except ValidationError as e:
                abort(400, message='There was an error while trying to create your account -> {}'.format(e.message))
    

    앞에서 언급했듯이 대부분의 로직 및 데이터베이스 상호 작용이 모델에 있기 때문에 컨트롤러 논리는 비교적 간단합니다.

    AuthLogin 클래스의 post() 함수는 reqparse를 사용하여 필드의 유효성을 검사 한 다음 User.validate ()를 호출하여 전송 된 정보의 유효성을 검증하고 토큰. 오류가 발생하면 이를 포착하여 오류 메시지로 응답하는 역할을 합니다. 

    마찬가지로 AuthRegister의 경우 사용자로부터 정보를 수집하고 모델의 create () 함수를 호출합니다. 메일 주소, 암호, 암호 확인 및 전체 이름 필드에 대한 값을 User.create () 함수에 전달합니다. 이 함수 또한 뭔가 잘못된 것이 있으면 오류를 발생시킵니다.

    python run.py runserver를 사용하여 서버를 실행하여 테스트 해보세요. 여기서 만든 두 개의 엔드포인트에 액세스 할 수 있어야 하며 잘 작동할 것입니다.


    여기까지 인증에 관련된 작업을 했으며 이제 이 어플리케이션의 목적인 파일을 위한 모델을 만들어 봅시다.


    파일과 폴더 모델 

    우리는 User 모델로 했던 것과 비슷하게 파일 및 폴더 작업을 위한 간단한 모델을 만들 것입니다. File 모델의 자식으로 Folder 모델을 생성 하는 것을 확인 하세요.


    File model

    파일들은 파일 시스템에 편평하게 저장 됩니다. 모든 사용자는 그들의 파일이 저장되는 폴더를 갖게 되지만, 데이터 의 구조는 논리적이며 데이터베이스에 저장됩니다. 이렇게 하면 파일 시스템에 최소한의 쓰기 작업 만 수행 할 수 있게 됩니다. 이렇게 하기 위해 우리는 향후의 프로젝트를 위해 당신에게 유용 할 몇 가지 쿨한 기술을 사용 할 것입니다.

     /api/models.py 에 File과 Folder 라는 클래스 모델을 만듭니다.

    class File(RethinkDBModel):
        _table = 'files'
    
    class Folder(File):
        pass

    File 모델을 위한 create() 메소드를 생성함으로써 시작해보죠. 이 메소드는 파일을 만드는 데 사용되는 /users/ <user_id>/files/<file_id> 엔드 포인트에 대한  POST 요청을 할 때 호출됩니다.

    @classmethod
    def create(cls, **kwargs):
        name = kwargs.get('name')
        size = kwargs.get('size')
        uri = kwargs.get('uri')
        parent = kwargs.get('parent')
        creator = kwargs.get('creator')
    
        # Direct parent ID
        parent_id = '0' if parent is None else parent['id']
    
        doc = {
            'name': name,
            'size': size,
            'uri': uri,
            'parent_id': parent_id,
            'creator': creator,
            'is_folder': False,
            'status': True,
            'date_created': datetime.now(r.make_timezone('+01:00')),
            'date_modified': datetime.now(r.make_timezone('+01:00'))
        }
    
        res = r.table(cls._table).insert(doc).run(conn)
        doc['id'] = res['generated_keys'][0]
    
        if parent is not None:
            Folder.add_object(parent, doc['id'])
    
        return doc

    먼저 이름, 크기, 파일 URI, 작성자 등과 같은 키워드 인수 사전에서 필요한 모든 정보를 수집합니다. 우리는 부모라고 부르는 매개 변수를 수집했습니다. 이 필드는 이 파일을 저장할 폴더의 ID를 가르킵니다. 파일을 루트 폴더에 저장하려면 이 매개 변수를 전달하지 않도록 선택할 수도 있습니다. 계속 진행하면서 복잡한 중첩 폴더 구조를 만드는 방법을 이해해 봅시다.

    여기서 부모가 None 일 경우 parent_id 필드를 0 이 되는 것에 주목하십시오. 이것은 부모가 없는 파일이 생성되는 경우에 처리되며 이것은 0의 ID를 가진 루트 폴더에 파일을 저장하고 있다고 가정합니다.

    그래서 우리는 파일에 대한 모든 정보를 사전에 수집하고 insert () 함수를 호출하여 데이터베이스에 저장합니다. 삽입 함수 호출에서 리턴 된 사전에는 새로 생성 된 문서의 ID가 들어 있습니다. 사전을 삽입 한 후 ID 정보를 사전에 채워 사용자에게 반환 할 수 있습니다.

    이 메소드의 마지막 3 행에서 부모가 None인지 확인하기 위해 검사를 추가했습니다. 이 파일 관리자 구현에는 폴더가 있으므로 폴더에 파일을 만들 때마다 새로 만든 각 객체를 폴더에 논리적으로 추가해야합니다. 우리는 객체 ID를 저장하려는 폴더의 해당 레코드에 있는 객체 목록에 추가하여 이를 수행합니다. 우리는 add_object라는 Folder 모델에서 생성 할 메소드를 호출하여 이를 수행합니다.

    다음으로 우리는 기본 RethinkDBModel 클래스로 돌아가서 자식 클래스에서 재정의 할 수도 있고 하지 않을 수도 있는 유용한 메소드를 만듭니다.

    class RethinkDBModel(object):
        @classmethod
        def find(cls, id):
            return r.table(cls._table).get(id).run(conn)
    
        @classmethod
        def filter(cls, predicate):
            return list(r.table(cls._table).filter(predicate).run(conn))
    
        @classmethod
        def update(cls, id, fields):
            status = r.table(cls._table).get(id).update(fields).run(conn)
            if status['errors']:
                raise DatabaseProcessError("Could not complete the update action")
            return True
    
        @classmethod
        def delete(cls, id):
            status = r.table(cls._table).get(id).delete().run(conn)
            if status['errors']:
                raise DatabaseProcessError("Could not complete the delete action")
            return True

    여기에서는 RethinkDB get (), filter (), update () 및 delete () 함수에 대한 래퍼 메서드를 만들었습니다. 이러면  하위 클래스는 복잡한 상호 작용을 위해 이러한 함수를 그대로 활용 할 수 있습니다.

    파일 모델에서 다음에 만들 메소드는 폴더간에 파일을 이동하는 데 사용할 함수입니다.

    @classmethod
    def move(cls, obj, to):
        previous_folder_id = obj['parent_id']
        previous_folder = Folder.find(previous_folder_id)
        Folder.remove_object(previous_folder, obj['id'])
        Folder.add_object(to, obj['id'])

    이 로직은 매우 간단합니다. 이 메소드는 파일 obj를 폴더로 이동할 때 호출됩니다.

    먼저 파일의 현재 상위 디렉토리에 대한 현재 폴더 ID를 가져옵니다. 폴더 모델 찾기 함수를 호출하여 폴더 객체를 previous_folder 로 얻은 다음 우리는 두 가지 작업을 수행 합니다. previous_folder에서 이동 시킬 파일 객체를 제거하고 새 폴더에 추가합니다. Folder 클래스의 remove_object () 및 add_object () 메서드를 호출하여 이 작업을 수행 할 수 있습니다. 

    이제 데이터베이스 작성, 편집, 삭제 등과 같은 파일에 대한 기본 상호 작용을 수행 할 수 있게 되었습니다. 다음으로 우리는 파일에 대한 작업과 매우 유사한 폴더 모델에 대한 로직을 봅시다.


    Folder model

    @classmethod
    def create(cls, **kwargs):
        name = kwargs.get('name')
        parent = kwargs.get('parent')
        creator = kwargs.get('creator')
    
        # Direct parent ID
        parent_id = '0' if parent is None else parent['id']
    
        doc = {
            'name': name,
            'parent_id': parent_id,
            'creator': creator,
            'is_folder': True,
            'last_index': 0,
            'status': True,
            'objects': None,
            'date_created': datetime.now(r.make_timezone('+01:00')),
            'date_modified': datetime.now(r.make_timezone('+01:00'))
        }
    
        res = r.table(cls._table).insert(doc).run(conn)
        doc['id'] = res['generated_keys'][0]
    
        if parent is not None:
            cls.add_object(parent, doc['id'], True)
    
        cls.tag_folder(parent, doc['id'])
    
        return doc
    
    @classmethod
    def tag_folder(cls, parent, id):
        tag = id if parent is None else '{}#{}'.format(parent['tag'], parent['last_index'])
        cls.update(id, {'tag': tag})

    여기에 있는 create() 메소드는 몇 가지를 제외하곤는 파일과 매우 유사합니다. 가장 먼저 알아야 할 것은 폴더를 만들 때 단지 폴더의 이름과 작성자만 있으면 된다는 것입니다. 우리는 부모 폴더를 결정하기 위해 비슷한 논리를 사용했으며, 결국 add_object () 메서드를 통해 부모 폴더에 폴더를 추가했습니다.

    여기서 is_folder 필드는 기본적으로 폴더의 경우 True로, 파일의 경우 False로 설정됩니다.

    tag_folder () 는 폴더 이동과 관련하여 나중에 이 작업이 필요할 것입니다. 요약하면 폴더는 파일 트리의 위치에 따라 태그가 지정됩니다. 인덱스는 트리에서의 레벨을 기반으로 합니다. 루트 레벨에 저장된 폴더에는 <id> 태그가 있습니다. 여기서 id는 폴더의 ID입니다. 해당 폴더 아래에 저장된 폴더의 ID는 <id> -n이며, 여기서 n은 계속 증가하는 정수입니다. 결과적으로 중첩 된 폴더는 동일한 패턴을 따르고 <id> -nm 등의 ID를 갖습니다. 폴더를 더 추가 할 때 n이 변경되며 기본값으로 0 인 각 폴더의 last_index 필드에 필요한 데이터가 저장됩니다. 폴더를 이 폴더에 추가하면 last_index 값이 증가합니다. tag_folder () 메서드는 이 모든 것을 처리합니다.

    insert () 함수를 호출하여 생성 한 모든 데이터를 데이터베이스에 저장합니다.

    다음으로 폴더의 목록 정보를 표시하는 기능을 포함하도록 File 클래스의 find 메서드를 재정의합니다. 나중에 프런트 엔드에 유용합니다.

    @classmethod
    def find(cls, id, listing=False):
        file_ref = r.table(cls._table).get(id).run(conn)
        if file_ref is not None:
            if file_ref['is_folder'] and listing and file_ref['objects'] is not None:
                file_ref['objects'] = list(r.table(cls._table).get_all(r.args(file_ref['objects'])).run(conn))
        return file_ref

    다음 3 가지 조건을 만족하는 폴더 목록을 보여줍니다.

    • 목록이 True로 설정됩니다. 이 변수를 사용하여 폴더에 포함 된 파일에 대한 정보가 실제로 필요한지 여부를 알 수 있습니다.

    • file_ref 객체는 실제로 폴더입니다. 문서의 is_folder 필드를 확인하여 이를 확인합니다.

    • 폴더 문서의 개체 목록 내에 개체가 있습니다.

    이러한 모든 조건이 충족되면 파일 테이블에서 get_all 메서드를 호출하여 중첩 된 모든 객체를 가져옵니다. 이 메서드는 여러 키를 받아들이고 해당 키가 있는 모든 개체를 반환합니다. 우리는 r.args 메서드를 사용하여 객체 목록을 get_all 메서드의 여러 인수로 변환합니다. 문서의 객체 필드를 반환 된 목록으로 대체합니다. 이 목록에는 각 중첩 된 파일 / 폴더의 세부 정보가 들어 있습니다.

    다음으로 이동하여 폴더의 이동 방법을 만듭니다. 이는 태그 작업을 위한 로직을 포함하며 이전에 작성한 파일의 이동 방법과 매우 유사합니다.


    @classmethod
    def move(cls, obj, to):
        if to is not None:
            parent_tag = to['tag']
            child_tag = obj['tag']
    
            parent_sections = parent_tag.split("#")
            child_sections = child_tag.split("#")
    
            if len(parent_sections) > len(child_sections):
                matches = re.match(child_tag, parent_tag)
                if matches is not None:
                    raise Exception("You can't move this object to the specified folder")
    
        previous_folder_id = obj['parent_id']
        previous_folder = cls.find(previous_folder_id)
        cls.remove_object(previous_folder, obj['id'])
    
        if to is not None:
            cls.add_object(to, obj['id'], True)

    여기서는 먼저 이동하려는 폴더가 실제로 지정되었고 None이 아님을 확인합니다. 이것은 여기에 가정이 지정되지 않은 경우 실제로 이 폴더를 루트 폴더로 이동한다는 가정 때문입니다.

    우리는 이동하려는 폴더의 태그를 얻으며 이동하려는 폴더의 태그를 가져오고 태그의 섹션 수를 비교합니다. 이것은 우리가 파일 트리에서 폴더의 레벨을 아는 방법입니다. 이동이 그리 간단하지 않은 경우가 하나뿐인데. 즉, 하위 섹션보다 상위 섹션이 있는 경우입니다. (이 경우 부모는 이 폴더를 이동하려는 폴더를 나타냅니다.) 우리는 폴더를 그 이상의 레벨에 있는 폴더로 이동할 수 있지만, parent_sections가 child_sections 이상인 경우,이 폴더를 이동하려는 폴더가 자체 폴더에 중첩되어 있을 가능성이 있음을 알고 있습니다. 앞에서 언급했듯이 폴더 구조는 순전히 논리적이며 오류가 없는지 확인해야하기 때문에 매우 주의해야 합니다.

    이동하려는 폴더가 파일 트리에서 이동하는 폴더 아래에있는 경우 이전 폴더가 후자에 중첩되어 있지 않은지 확인해야합니다. 이것은 단순히 우리가 움직이는 폴더의 child_tag을 보장함으로써 이루어질 수 있으며, parent_tag 문자열을 시작하지 않습니다. 우리는 이것을 구현하기 위해 regex를 사용하고 이런 일이 발생하면 예외를 발생시킵니다.

    조금 만 더 집중해 봅시다.거의 끝났습니다! 

    마지막으로는 앞서 언급 한 add_object () 및 remove_object () 메소드를 작성해 봅시다.

    @classmethod
    def remove_object(cls, folder, object_id):
        update_fields = folder['objects'] or []
        while object_id in update_fields:
            update_fields.remove(object_id)
        cls.update(folder['id'], {'objects': update_fields})
    
    @classmethod
    def add_object(cls, folder, object_id, is_folder=False):
        p = {}
        update_fields = folder['objects'] or []
        update_fields.append(object_id)
        if is_folder:
            p['last_index'] = folder['last_index'] + 1
        p['objects'] = update_fields
        cls.update(folder['id'], p)

    앞서 언급했듯이 폴더 개체의 개체 목록을 수정하여 추가 및 제거 작업을 수행합니다. 하위 폴더를 추가 할 때 폴더의 last_index 변수를 업데이트하는 제약 조건을 적용합니다.

    이제 모델을 완성합니다. 컨트롤러로 이동!


    파일 컨트롤러 

    파일 컨트롤러는 파일과 폴더 작업에 사용되므로 이전 컨트롤러보다 약간 더 많은 로직이 있습니다. /api/controllers/files.py 모듈에 컨트롤러에 대한 상용구를 만드는 것으로 시작합니다.

    import os
    
    from flask import request, g
    from flask_restful import reqparse, abort, Resource
    from werkzeug import secure_filename
    
    from api.models import File
    
    BASE_DIR = os.path.abspath(
        os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
    )
    
    
    class CreateList(Resource):
        def get(self, user_id):
            pass
    
        def post(self, user_id):
            pass
    
    class ViewEditDelete(Resource):
        def get(self, user_id, file_id):
            pass
    
        def put(self, user_id, file_id):
            pass
    
        def delete(self, user_id, file_id):
            pass

    이름에서 알 수 있듯이 CreateList 클래스는 로그인 한 사용자의 파일을 만들고 나열하는 데 사용됩니다. ViewEditDelete 클래스는 이름에서 알 수 있듯이 파일보기, 편집 및 삭제에 사용됩니다. 클래스에서 사용하는 메소드는 적절한 HTTP 동작과 일치합니다.

    우리는 Resource 클래스의 메소드에서 사용 할 데코레이터를 만들어서 구현을 시작합니다. 이를 /api/utils/decorators.py 모듈로 분리하려고 합니다.

    from jose import jwt
    from jose.exceptions import JWTError
    from functools import wraps
    
    from flask import current_app, request, g
    from flask_restful import abort
    
    from api.models import User, File
    
    def login_required(f):
        '''
        This decorator checks the header to ensure a valid token is set
        '''
        @wraps(f)
        def func(*args, **kwargs):
            try:
                if 'authorization' not in request.headers:
                    abort(404, message="You need to be logged in to access this resource")
                token = request.headers.get('authorization')
                payload = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=['HS256'])
                user_id = payload['id']
                g.user = User.find(user_id)
                if g.user is None:
                   abort(404, message="The user id is invalid")
                return f(*args, **kwargs)
            except JWTError as e:
                abort(400, message="There was a problem while trying to parse your token -> {}".format(e.message))
        return func
    
    def validate_user(f):
        '''
        This decorate ensures that the user logged in is the actually the same user we're operating on
        '''
        @wraps(f)
        def func(*args, **kwargs):
            user_id = kwargs.get('user_id')
            if user_id != g.user['id']:
                abort(404, message="You do not have permission to the resource you are trying to access")
            return f(*args, **kwargs)
        return func
    
    def belongs_to_user(f):
        '''
        This decorator ensures that the file we're trying to access actually belongs to us
        '''
        @wraps(f)
        def func(*args, **kwargs):
            file_id = kwargs.get('file_id')
            user_id = kwargs.get('user_id')
            file = File.find(file_id, True)
            if not file or file['creator'] != user_id:
                abort(404, message="The file you are trying to access was not found")
            g.file = file
            return f(*args, **kwargs)
        return func
    • login_required 데코레이터는 메소드의 기능에 액세스하기 전에 사용자가 실제로 로그인했는지 확인하는 데 사용됩니다. 우리는이 장식자를 사용하여 유효성을 보장하기 위해 토큰을 디코딩하여 특정 끝점을 보호합니다. id 필드가 토큰에 저장되고 해당 사용자 객체를 검색하려고 시도합니다. 또한 메소드 정의 내에서 액세스 할 수 있도록이 객체를 g.user에 저장합니다.

    • 마찬가지로 다른 사용자가 다른 사용자의 ID로 레이블 된 URL 패턴에 액세스 할 수 없도록하는 validate_user 데코레이터를 만듭니다. 이 유효성은 순수하게 URL의 정보를 기반으로합니다.

    • 마지막으로 belongs_to_user 데코레이터는 파일을 만든 사용자 만 파일에 액세스 할 수 있도록합니다. 이 데코레이터는 파일 문서의 작성자 필드를 제공된 user_id와 대조하여 실제로 검사합니다.

    다음은 새 파일 작성 및 파일 목록보기입니다.

    class CreateList(Resource):
        @login_required
        @validate_user
        @marshal_with(file_array_serializer)
        def get(self, user_id):
            try:
                return File.filter({'creator': user_id, 'parent_id': '0'})
            except Exception as e:
                abort(500, message="There was an error while trying to get your files --> {}".format(e.message))
    
        @login_required
        @validate_user
        @marshal_with(file_serializer)
        def post(self, user_id):
            try:
                parser = reqparse.RequestParser()
                parser.add_argument('name', type=str, help="This should be the folder name if creating a folder")
                parser.add_argument('parent_id', type=str, help='This should be the parent folder id')
                parser.add_argument('is_folder', type=bool, help="This indicates whether you are trying to create a folder or not")
    
                args = parser.parse_args()
    
                name = args.get('name', None)
                parent_id = args.get('parent_id', None)
                is_folder =  args.get('is_folder', False)
    
                parent = None
    
                # Are we adding this to a parent folder?
                if parent_id is not None:
                    parent = File.find(parent_id)
                    if parent is None:
                        raise Exception("This folder does not exist")
                    if not parent['is_folder']:
                        raise Exception("Select a valid folder to upload to")
    
                # Are we creating a folder?
                if is_folder:
                    if name is None:
                        raise Exception("You need to specify a name for this folder")
    
                    return Folder.create(
                        name=name,
                        parent=parent,
                        is_folder=is_folder,
                        creator=user_id
                    )
                else:
                    files = request.files['file']
    
                    if files and is_allowed(files.filename):
                        _dir = os.path.join(BASE_DIR, 'upload/{}/'.format(user_id))
    
                        if not os.path.isdir(_dir):
                            os.mkdir(_dir)
    
                        filename = secure_filename(files.filename)
                        to_path = os.path.join(_dir, filename)
                        files.save(to_path)
                        fileuri = os.path.join('upload/{}/'.format(user_id), filename)
                        filesize = os.path.getsize(to_path)
    
                        return File.create(
                            name=filename,
                            uri=fileuri,
                            size=filesize,
                            parent=parent,
                            creator=user_id
                        )
                    raise Exception("You did not supply a valid file in your request")
            except Exception as e:
                abort(500, message="There was an error while processing your request --> {}".format(e.message))

    목록 작성 방법은 매우 간단합니다. 특정 사용자가 작성하고 루트 디렉토리에 저장 한 모든 파일에 대해 표를 필터링하는 것뿐입니다. 이 끝점에 대해이 데이터를 반환하고 오류가 있으면 예외를 throw합니다.

    생성 작업을 위해 우리는 많은 일을합니다. 이 자습서에서는 파일과 폴더가 동일한 끝점을 사용하여 만들어지는 것으로 가정합니다. 파일의 경우 폴더에 업로드하는 경우 parent_id와 함께 파일을 제공해야합니다. 폴더의 경우 이름과 parent_id 값이 필요합니다. 다른 폴더에서이 이름을 다시 생성해야합니다. 폴더의 경우 폴더를 만들도록 지정하라는 요청과 함께 is_folder 필드를 보내야합니다.

    이 파일을 폴더에 저장하려면 폴더가 존재하고 유효한 폴더인지 확인해야합니다. 또한 폴더를 만들면 이름 필드를 제공해야합니다.

    파일을 만들 때 앞서 언급 한 것처럼 다른 사용자를 위해 특별히 명명 된 폴더에 파일을 업로드합니다. 여기서는 서로 다른 사용자 파일 디렉토리에 / upload / <user_id> 패턴을 사용하고 있습니다. 또한 파일 정보를 사용하여 테이블에 저장할 문서를 채 웁니다.

    파일과 폴더 생성을위한 각각의 메소드 인 File.create ()와 Folder.create ()를 호출하여 결론을 맺습니다.

    Flask-RESTful과 함께 사용할 수있는 marshal_with 데코레이터를 사용했음을 주목하십시오. 이 데코레이터는 응답 객체의 형식을 지정하고 반환 할 다른 필드 이름과 유형을 나타내는 데 사용됩니다. 아래 file_array_serializer 및 file_serializer의 정의를 참조하십시오.

    file_array_serializer = {
        'id': fields.String,
        'name': fields.String,
        'size': fields.Integer,
        'uri': fields.String,
        'is_folder': fields.Boolean,
        'parent_id': fields.String,
        'creator': fields.String,
        'date_created': fields.DateTime(dt_format=  'rfc822'),
        'date_modified': fields.DateTime(dt_format='rfc822'),
    }
    
    file_serializer = {
        'id': fields.String,
        'name': fields.String,
        'size': fields.Integer,
        'uri': fields.String,
        'is_folder': fields.Boolean,
        'objects': fields.Nested(file_array_serializer, default=[]),
        'parent_id': fields.String,
        'creator': fields.String,
        'date_created': fields.DateTime(dt_format='rfc822'),
        'date_modified': fields.DateTime(dt_format='rfc822'),
    }

    이것은 /api/controllers/files.py 모듈 또는 별도의 /api/utils/serializers.py 모듈의 맨 위에 추가 할 수 있습니다.

    두 serializer의 차이점은 파일 serializer가 응답에 objects 배열을 포함한다는 것입니다. 우리는 객체 응답에 file_serializer를 사용하는 동안 list 응답에 file_array_serializer를 사용합니다.

    또한 is_allowed ()라는 함수를 사용하여 업로드하는 모든 파일을 지원할 수 있도록했습니다. 허용 된 모든 확장자 목록을 포함하는 ALLOWED_EXTENSIONS라는 목록을 만들었습니다.

    ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])
    
    def is_allowed(filename):
        return '.' in filename and \
               filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
    


    마지막으로 ViewEditDelete의 리소스 클래스에 /api/controllers/files.py 모듈을 추가하여 결론을 맺습니다.

    class ViewEditDelete(Resource):
        @login_required
        @validate_user
        @belongs_to_user
        @marshal_with(file_serializer)
        def get(self, user_id, file_id):
            try:
                should_download = request.args.get('download', False)
                if should_download == 'true':
                    parts = os.path.split(g.file['uri'])
                    return send_from_directory(directory=parts[0], filename=parts[1])
                return g.file
            except Exception as e:
                abort(500, message="There was an while processing your request --> {}".format(e.message))
    
        @login_required
        @validate_user
        @belongs_to_user
        @marshal_with(file_serializer)
        def put(self, user_id, file_id):
            try:
                update_fields = {}
                parser = reqparse.RequestParser()
    
                parser.add_argument('name', type=str, help="New name for the file/folder")
                parser.add_argument('parent_id', type=str, help="New parent folder for the file/folder")
    
                args = parser.parse_args()
    
                name = args.get('name', None)
                parent_id = args.get('parent_id', None)
    
                if name is not None:
                    update_fields['name'] = name
    
                if parent_id is not None and g.file['parent_id'] != parent_id:
                    if parent_id != '0'
                        folder_access = Folder.filter({'id': parent_id, 'creator': user_id})
                        if not folder_access:
                            abort(404, message="You don't have access to the folder you're trying to move this object to")
    
                    if g.file['is_folder']:
                        update_fields['tag'] = g.file['id'] if parent_id == '0' else '{}#{}'.format(folder_access['tag'], folder['last_index'])
                        Folder.move(g.file, folder_access)
                    else:
                        File.move(g.file, folder_access)
    
                    update_fields['parent_id'] = parent_id
    
                if g.file['is_folder']:
                    Folder.update(file_id, update_fields)
                else:
                    File.update(file_id, update_fields)
    
                return File.find(file_id)
            except Exception as e:
                abort(500, message="There was an while processing your request --> {}".format(e.message))
    
        @login_required
        @validate_user
        @belongs_to_user
        def delete(self, user_id, file_id):
            try:
                hard_delete = request.args.get('hard_delete', False)
                if not g.file['is_folder']:
                    if hard_delete == 'true':
                        os.remove(g.file['uri'])
                        File.delete(file_id)
                    else:
                        File.update(file_id, {'status': False})
                else:
                    if hard_delete == 'true':
                        folders = Folder.filter(lambda folder: folder['tag'].startswith(g.file['tag']))
                        for folder in folders:
                            files = File.filter({'parent_id': folder['id'], 'is_folder': False })
                            File.delete_where({'parent_id': folder['id'], 'is_folder': False })
                            for f in files:
                                os.remove(f['uri'])
                    else:
                        File.update(file_id, {'status': False})
                        File.update_where({'parent_id': file_id}, {'status': False})
                return "File has been deleted successfully", 204
            except:
                abort(500, message="There was an error while processing your request --> {}".format(e.message))

    id를 기반으로 하나의 파일 또는 폴더 객체를 반환하는 get () 메서드를 만들었습니다. 폴더의 경우 목록 정보가 포함됩니다. belongs_to_user 데코레이터를 보면 이것이 어떻게 수행되는지 볼 수 있습니다. 파일의 경우 파일을 다운로드하려는 경우 should_download라는 쿼리 매개 변수를 true로 설정했습니다.

    put () 메서드는 파일 및 폴더 정보를 업데이트합니다. 여기에는 파일 및 폴더 이동도 포함됩니다. 파일 이동은 파일 / 폴더의 parent_id 필드를 업데이트하여 실행됩니다. 두 가지에 대한 논리는 파일 및 폴더 모델의 move () 메서드에서 다룹니다.

    delete () 메서드는 또한 하드 삭제를 수행할지 여부를 지정하는 쿼리 매개 변수입니다. 하드 h 제의 경우, 레코드가 데이터베이스에서 제거되고 파일 시스템에서 파일이 h 제됩니다. 소프트 삭제의 경우 파일 상태 필드를 false로만 업데이트합니다.

    테이블에서 필터링 된 집합을 삭제하고 업데이트하기 위해 RethinkDBModel 클래스에서 update_where () 및 delete_where ()라는 새 메서드를 만들었습니다.

    @classmethod
    def update_where(cls, predicate, fields):
        status = r.table(cls._table).filter(predicate).update(fields).run(conn)
        if status['errors']:
            raise DatabaseProcessError("Could not complete the update action")
        return True
    
    @classmethod
    def delete_where(cls, predicate):
        status = r.table(cls._table).filter(predicate).delete().run(conn)
        if status['errors']:
            raise DatabaseProcessError("Could not complete the delete action")
        return True
    

    그리고 그게 다입니다!!! 파일 저장 API가 끝났습니다. API를 실행하여 실제로 작동하는지 확인하십시오.

    다음 튜토리얼에서는 VueJS를 사용하여 파일 저장소 API에 대한 프론트 엔드 개발에 대해 다룰 것입니다.

    여기서  here .  프로젝트의 코드베이스를 확인할 수 있습니다.


    *외부패키지리스트*

    aniso8601==1.2.0

    click==6.7

    ecdsa==0.13

    Flask==0.12

    Flask-Cache==0.13.1

    Flask-RESTful==0.3.5

    Flask-Script==2.0.5

    future==0.16.0

    itsdangerous==0.24

    Jinja2==2.9.5

    MarkupSafe==0.23

    passlib==1.7.1

    pycrypto==2.6.1

    python-dateutil==2.6.0

    python-jose==1.3.2

    pytz==2016.10

    rethinkdb==2.3.0.post6

    six==1.10.0

    Werkzeug==0.11.15

    wheel==0.24.0




    안타깝게도 프런트 엔드를 다루는 파트2 는 아직 안나왔습니다.


    아래에 훌륭한 Flask 한글메뉴얼이 존재하며, Flask 를 배우기에 최고의 장소입니다.
    링크 : http://flask-docs-kr.readthedocs.io/ko/latest/index.html


    Applications Flask 웹어플리케이션 구축하기 


    PostedJanuary 16, 2014

    소개

    파이썬 웹 애플리케이션을 구축하는 데는 여러 가지 방법과 관습이 있습니다. 특정 프레임 워크는 작업을 자동화하고 쉽게하기위한 도구(스캐폴딩용)와 함께 출하 되지만 코드세트가 관련 파일 및 폴더에 [논리적으로] 배포되면 거의 모든 솔루션이 패키지/모듈화 응용 프로그램에 의존합니다.

    미니멀리스트 웹 애플리케이션 개발 프레임워크 인 Flask에는 자체 청사진(blueprints)이 있습니다.

    본 DigitalOcean 기사에서는 응용 프로그램 디렉토리를 만드는 방법과 Flask의 청사진으로 만든 재사용 가능한 구성 요소로 작업하는 방법을 살펴 보겠습니다. 이 조각들로 응용 프로그램 구성 요소의 개발 및 유지보수를 (단순화 되고) 하게 됩니다.

    Flask: 미니멀리즘 웹 어플리케이션 개발 프레임워크 

    플라스크 (Flask)는 크리티컬 한 것들을 다루는 방식을 강요하는 것을 삼가는 미니멀리즘 (또는 마이크로) 프레임워크입니다. 대신, Flask는 개발자가 원하고 익숙한 도구를 선택해서 사용할 수 있게 합니다. 이를 위해 자체 확장 색인이 있으며  log-ins부터 logging에 이르기까지 모든 것을 처리 할 수 있는 도구는 이미 많이 있습니다.

    엄격하게는 "전통적인" 프레임 워크가 아니며 구성 파일에 부분적으로 의존합니다. 구성 파일은 프로젝트를 시작하고 유지할 때 많은 것을 편하고 단순하게 하게 합니다. Flask 는 군더더기가 없습니다.

    이 글을 위한 선택요소들 

    이전 섹션에서 말했듯이, 플라스크 방식은 당신이 느끼는 가장 편한 도구를 사용하는 것과 관련이 있습니다. 이 글에서는 확장 기능 및 라이브러리 (즉, 데이터베이스 추출 레이어) 측면에서 가장 일반적인 (현명한) 선택 방법을 사용합니다. 이러한 선택은 다음을 포함합니다 :

    • SQLAlchemy (via Flask-SQLAlchemy)

    • WTForms (via Flask-WTF)

    Flask-SQLAlchemy

    Flask에 SQLAlchemy 지원을 추가합니다. 빠르고 쉽습니다.

    승인 된 확장 프로그램입니다.

    Author: Armin Ronacher
    PyPI Page: Flask-SQLAlchemy
    Documentation: Read docs @ packages.python.org
    On Github: [mitsuhiko/flask-sqlalchemy](https://github.com/mitsuhiko/flask-sqlalchemy)
    

    Flask-WTF

    Flask-WTF는 WTForm과의 간단한 통합을 제공합니다. 이 통합에는보다 강력한 보안을 위해 선택적 CSRF 처리가 포함됩니다.

    승인 된 확장 프로그램입니다.

    Author: Anthony Ford (created by Dan Jacob)
    PyPI Page: Flask-WTF
    Documentation: Read docs @ packages.python.org
    On Github: [ajford/flask-wtf](https://github.com/mitsuhiko/flask-wtf)
    


    Flask 를 위한 시스템 준비

    Flask 응용 프로그램을 구성하기 전에 시스템을 준비하고 Flask 배포본을 다운로드(및 설치)합시다.

    참고 : 우리는 최신 버전의 사용 가능한 운영 체제 (예 : Ubuntu 13)를 실행하는 새로 인스턴스화 된 드롭 릿에 대해 작업 할 것입니다. 새로운 시스템에서도 모든 것을 테스트하는 것이 좋습니다. 특히 고객에게 적극적으로 서비스하는 경우 더욱 그렇습니다.

    OS 준비

    안정적인 서버를 갖추려면 모든 관련 도구와 라이브러리를 최신 상태로 유지 관리해야합니다.
    사용 가능한 최신 버전의 기본 응용 프로그램을 확보하려면 업데이트부터 시작합시다.
    데비안 기반 시스템 (즉, 우분투, 데비안)에서 다음을 실행하십시오 :

    aptitude    update
    aptitude -y upgrade

    필요한 개발 도구를 얻으려면 다음 명령을 사용하여 "build-essential"을 설치하십시오.

    aptitude install -y build-essential python-dev python2.7-dev
    

    Setting up Python, pip and virtualenv

    Ubuntu와 Debian에는 기본적으로 사용할 수있는 Python 인터프리터의 최신 버전이 있습니다. 제한된 수의 패키지만 설치하면됩니다.

    • python-dev (development tools)

    • pip (to manage packages)

    • virtualenv (to create isolated, virtual

    참고 : 여기에 제공된 지침은 간략하게 유지됩니다. 자세한 내용은 일반적인 Python 도구 : virtualenv 사용, Pip와 함께 설치 및 패키지 관리의 pip 및 virtualenv에 대한 방법 문서를 참조하십시오.

    pip

    pip는 우리가 필요로하는 응용 프로그램 패키지를 설치하는 데 도움이되는 패키지 관리자입니다.
    pip를 설치하려면 다음 명령을 실행하십시오.

    curl https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py | python -
    curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python -
    export PATH="/usr/local/bin:$PATH"
    

    virtualenv

    모든 의존성과 함께 자체 환경 내에서 Python 응용 프로그램을 포함하는 것이 가장 좋습니다. 환경은 모든 것이 상주하는 격리 된 위치 (디렉토리)로 (간단한 용어로) 가장 잘 설명 될 수 있습니다. 이를 위해 virtualenv라는 도구가 사용됩니다.

    pip를 사용하여 virtualenv를 설치하려면 다음을 실행하십시오.

    sudo pip install virtualenv
    


    어플리케이션 디렉토리 구성 


    LargeApp 같은 명백한 이름을 응용 프로그램 폴더로 사용합니다. 내부에서 우리는 애플리케이션 패키지 (예 : 앱)와 테스트 환경 (개발) 서버를 실행하기위한 "run.py"와 같은 다른 가상 환경 (예 : env)을 유지하려고합니다. 

    아래 예제처럼 제공되는 구조는 매우 확장 성이 뛰어나며 Flask 및 기타 라이브러리에서 제공하는 유용한 도구를 모두 사용할 수 있도록 제작되었습니다. 우리는 모든 것을 모든 것을 구성함으로써 단계적으로 설명합니다.

    목표 예제 구조 :

    ~/LargeApp
        |-- run.py
        |-- config.py
        |__ /env             # Virtual Environment
        |__ /app             # Our Application Module
             |-- __init__.py
             |-- /module_one
                 |-- __init__.py
                 |-- controllers.py
                 |-- models.py                
             |__ /templates
                 |__ /module_one
                     |-- hello.html
             |__ /static
             |__ ..
             |__ .
        |__ ..
        |__ .
    

    어플리케이션 폴더 생성

    우리가 필요로하는 메인 폴더를 만드는 것으로 시작합시다.작업을 수행하려면 다음 명령을 연속적으로 실행하십시오.

    mkdir ~/LargeApp
    mkdir ~/LargeApp/app
    mkdir ~/LargeApp/app/templates
    mkdir ~/LargeApp/app/static    
    

    현재구조는 이렇습니다.

    ~/LargeApp
        |__ /app             # Our Application Module
             |__ /templates
             |__ /static
    

    가상환경 생성하기

    가상 환경을 사용하면 많은 이점을 얻을 수 있습니다. 각 응용 프로그램에 대해 새 가상 환경을 사용하는 것이 좋습니다. 응용 프로그램 내부에 virtualenv 폴더를 유지하면 순서대로 정리할 수있는 좋은 방법입니다.

    다음을 실행하여 pip가 설치된 새 가상 환경을 만듭니다.

    cd         ~/LargeApp
    virtualenv env
    

    어플리케이션 파일들 생성하기

    이 단계에서는 모듈 및 청사진으로 작업하기 전에 기본 응용 프로그램 파일을 구성합니다.
    기본 응용 프로그램 파일을 만들려면 다음을 실행하십시오.

    touch ~/LargeApp/run.py
    touch ~/LargeApp/config.py
    touch ~/LargeApp/app/__init__.py
    

    현재까지의 구조는 이렇습니다.

    ~/LargeApp
        |-- run.py
        |-- config.py
        |__ /env             # Virtual Environment
        |__ /app             # Our Application Module
             |-- __init__.py
             |__ /templates
             |__ /static
    

    플라스크 인스톨링 과 어플리케이션 디펜던시 

    일단 우리가 모든 것을 갖추었다면, Flask로 개발을 시작하기 위해 pip를 사용하여 다운로드하고 설치합시다. Flask를 가상 환경 env에 설치하려면 다음을 실행하십시오.

    cd ~/LargeApp
    env/bin/pip install flask
    env/bin/pip install flask-sqlalchemy
    env/bin/pip install flask-wtf
    

    참고 : 여기에서는 가상 환경을 활성화하지 않고 Flask를 다운로드하고 설치합니다. 그러나 우리는 가상환경 자체에서의 pip 을 사용하고 있기 때문에 동일한 작업을 수행합니다. 활성화 된 환경에서 작업하는 경우 대신 그냥 pip를 사용할 수 있습니다.

    이제 청사진을 사용하여 모듈화 된 더 큰 Flask 응용 프로그램을 만들 준비가되었습니다.


    모듈 & 블루프린트(컴포넌트)와 작업하기 

    모듈 기본

    이 시점에서 우리는 응용 프로그램 구조를 설정하고 종속성을 다운로드하여 준비했습니다.우리의 목표는 논리적으로 그룹화 할 수있는 모든 관련 모듈을 모듈화 (즉, Flask의 청사진으로 재사용 가능한 구성 요소 생성)하는 것입니다.

    이에 대한 예는 인증 시스템이 될 수 있습니다. 모든 뷰, 컨트롤러, 모델 및 헬퍼를 한 곳에서 사용하는 것은 재사용을 허용하는 방식으로 설정되므로 이러한 종류의 구조화는 생산성을 높이는 동시에 응용 프로그램을 유지 관리하는 좋은 방법입니다.

    대상 예제 모듈 (구성 요소) 구조 (inside / app) :

    # Our module example here is called *mod_auth*
    # You can name them as you like as long as conventions are followed
    
    /mod_auth
        |-- __init__.py
        |-- controllers.py
        |-- models.py
        |-- ..
        |-- .
    

    모듈 템플릿

    to-the-Max로 모듈화를 지원하기 위해 위의 규칙을 따르고 템플릿 파일을 포함 할 새 폴더 (모듈과 관련되거나 같음)를 포함하도록 "templates"폴더를 구성합니다.

    대상 예제 템플릿 디렉토리 구조 (LargeApp 내부) :

    /templates
        |-- 404.html
        |__ /auth
             |-- signin.html
             |-- signup.html
             |-- forgot.html
             |-- ..
             |-- .
    


    어플리케이션 만들기 

    이 섹션에서는 이전 단계를 계속 진행하고 응용 프로그램의 실제 코딩부터 시작하여 첫 번째 모듈화 된 구성 요소 (청사진 사용)로 이동합니다. 모든 인증 관련 절차 (예 : 서명, 서명 등)를 처리하는 mod_auth).

    “run.py” 을 편집합니다.

    nano ~/LargeApp/run.py
    

    다음을 추가하세요

    # Run a test server.
    from app import app
    app.run(host='0.0.0.0', port=8080, debug=True)
    


    “config.py” 를 편집합니다.

    nano ~/LargeApp/config.py
    

    다음 내용을 추가하세요.

    # Statement for enabling the development environment
    DEBUG = True
    
    # Define the application directory
    import os
    BASE_DIR = os.path.abspath(os.path.dirname(__file__))  
    
    # Define the database - we are working with
    # SQLite for this example
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(BASE_DIR, 'app.db')
    DATABASE_CONNECT_OPTIONS = {}
    
    # Application threads. A common general assumption is
    # using 2 per available processor cores - to handle
    # incoming requests using one and performing background
    # operations using the other.
    THREADS_PER_PAGE = 2
    
    # Enable protection agains *Cross-site Request Forgery (CSRF)*
    CSRF_ENABLED     = True
    
    # Use a secure, unique and absolutely secret key for
    # signing the data. 
    CSRF_SESSION_KEY = "secret"
    
    # Secret key for signing cookies
    SECRET_KEY = "secret"
    


    모듈과 컴포넌트 만들기

    이 섹션은이 기사의 핵심을 정의하는 첫 번째 주요 단계입니다. 여기서는 Flask의 청사진을 사용하여 모듈 (즉, 구성 요소)을 만드는 방법을 살펴 보겠습니다.

    이것에 대해 훌륭한 점은 제공되는 이식성과 코드의 재사용 성을 유지 보수 용이성과 함께 제공한다는 것입니다. 앞으로 남겨질 때 상황을 되돌아보고 이해하는 것이 어려울 때가 많기 때문에 앞으로 감사 할 것입니다.

    Step 1: Structuring The Module

    앞서 살펴본 것처럼 mod_auth의 첫 번째 모듈 (mod_auth) 디렉토리와 파일을 만들어 작업을 시작하십시오.

    # Create the module directory inside the *app* module
    mkdir ~/LargeApp/app/mod_auth
    
    # Create where module's templates will reside
    mkdir ~/LargeApp/app/templates/auth
    
    # Create __init__.py to set the directory as a Python module
    touch ~/LargeApp/app/mod_auth/__init__.py
    
    # Create module's controllers and models etc.
    touch ~/LargeApp/app/mod_auth/controllers.py
    touch ~/LargeApp/app/mod_auth/models.py
    touch ~/LargeApp/app/mod_auth/forms.py
    
    # Create module's templates
    touch ~/LargeApp/app/templates/auth/signin.html
    
    # Create a HTTP 404 Error page
    touch ~/LargeApp/app/templates/404.html
    

    이 작업이 끝나면 폴더 구조가 다음과 같이 표시됩니다.

    ~/LargeApp
        |-- run.py
        |-- config.py
        |__ /env             # Virtual Environment
        |__ /app             # Our Application Module
             |-- __init__.py
             |-- /mod_auth   # Our first module, mod_auth
                 |-- __init__.py
                 |-- controllers.py
                 |-- models.py
                 |-- forms.py
             |__ /templates
                 |-- 404.html
                 |__ /auth
                     |-- signin.html
             |__ /static
    

    Step 2: Define The Module Data Model(s)

    nano ~/LargeApp/app/mod_auth/models.py

    아래에 설명을 추가하십시오 - 모범적 인 내용 :

    # Import the database object (db) from the main application module
    # We will define this inside /app/__init__.py in the next sections.
    from app import db
    
    # Define a base model for other database tables to inherit
    class Base(db.Model):
    
        __abstract__  = True
    
        id            = db.Column(db.Integer, primary_key=True)
        date_created  = db.Column(db.DateTime,  default=db.func.current_timestamp())
        date_modified = db.Column(db.DateTime,  default=db.func.current_timestamp(),
                                               onupdate=db.func.current_timestamp())
    
    # Define a User model
    class User(Base):
    
        __tablename__ = 'auth_user'
    
        # User Name
        name    = db.Column(db.String(128),  nullable=False)
    
        # Identification Data: email & password
        email    = db.Column(db.String(128),  nullable=False,
                                                unique=True)
        password = db.Column(db.String(192),  nullable=False)
    
        # Authorisation Data: role & status
        role     = db.Column(db.SmallInteger, nullable=False)
        status   = db.Column(db.SmallInteger, nullable=False)
    
        # New instance instantiation procedure
        def __init__(self, name, email, password):
    
            self.name     = name
            self.email    = email
            self.password = password
    
        def __repr__(self):
            return '<User %r>' % (self.name)                        

    Step 3: Define Module Forms

    nano ~/LargeApp/app/mod_auth/forms.py
    

    아래에 설명을 추가하십시오

    # Import Form and RecaptchaField (optional)
    from flask.ext.wtf import Form # , RecaptchaField
    
    # Import Form elements such as TextField and BooleanField (optional)
    from wtforms import TextField, PasswordField # BooleanField
    
    # Import Form validators
    from wtforms.validators import Required, Email, EqualTo
    
    
    # Define the login form (WTForms)
    
    class LoginForm(Form):
        email    = TextField('Email Address', [Email(),
                    Required(message='Forgot your email address?')])
        password = PasswordField('Password', [
                    Required(message='Must provide a password. ;-)')])

    Step 4: Define Application Controllers (Views)

    nano ~/LargeApp/app/mod_auth/controllers.py
    

    아래에 설명을 추가하십시오

    # Import flask dependencies
    from flask import Blueprint, request, render_template, \
                      flash, g, session, redirect, url_for
    
    # Import password / encryption helper tools
    from werkzeug import check_password_hash, generate_password_hash
    
    # Import the database object from the main app module
    from app import db
    
    # Import module forms
    from app.mod_auth.forms import LoginForm
    
    # Import module models (i.e. User)
    from app.mod_auth.models import User
    
    # Define the blueprint: 'auth', set its url prefix: app.url/auth
    mod_auth = Blueprint('auth', __name__, url_prefix='/auth')
    
    # Set the route and accepted methods
    @mod_auth.route('/signin/', methods=['GET', 'POST'])
    def signin():
    
        # If sign in form is submitted
        form = LoginForm(request.form)
    
        # Verify the sign in form
        if form.validate_on_submit():
    
            user = User.query.filter_by(email=form.email.data).first()
    
            if user and check_password_hash(user.password, form.password.data):
    
                session['user_id'] = user.id
    
                flash('Welcome %s' % user.name)
    
                return redirect(url_for('auth.home'))
    
            flash('Wrong email or password', 'error-message')
    
        return render_template("auth/signin.html", form=form)

    Step 5: Set Up The Application in “app/init.py”

    nano ~/LargeApp/app/__init__.py
    

    아래에 설명을 추가하십시오

    # Import flask and template operators
    from flask import Flask, render_template
    
    # Import SQLAlchemy
    from flask.ext.sqlalchemy import SQLAlchemy
    
    # Define the WSGI application object
    app = Flask(__name__)
    
    # Configurations
    app.config.from_object('config')
    
    # Define the database object which is imported
    # by modules and controllers
    db = SQLAlchemy(app)
    
    # Sample HTTP error handling
    @app.errorhandler(404)
    def not_found(error):
        return render_template('404.html'), 404
    
    # Import a module / component using its blueprint handler variable (mod_auth)
    from app.mod_auth.controllers import mod_auth as auth_module
    
    # Register blueprint(s)
    app.register_blueprint(auth_module)
    # app.register_blueprint(xyz_module)
    # ..
    
    # Build the database:
    # This will create the database file using SQLAlchemy
    db.create_all()

    Step 6: Create The Templates

    nano ~/LargeApp/app/templates/auth/signin.html
    

    아래에 설명을 추가하십시오

    {% macro render_field(field, placeholder=None) %}
    {% if field.errors %}
    <div>
    {% elif field.flags.error %}
    <div>
    {% else %}
    <div>
    {% endif %}
        {% set css_class = 'form-control ' + kwargs.pop('class', '') %}
        {{ field(class=css_class, placeholder=placeholder, **kwargs) }}
    </div>
    {% endmacro %}
    
    <div>
      <div>
        <legend>Sign in</legend>
        {% with errors = get_flashed_messages(category_filter=["error"]) %}
        {% if errors %}
        <div>
        {% for error in errors %}
        {{ error }}<br>
        {% endfor %}
        </div>
        {% endif %}
        {% endwith %}
    
        {% if form.errors %}
        <div>
        {% for field, error in form.errors.items() %}
        {% for e in error %}
        {{ e }}<br>
        {% endfor %}
        {% endfor %}
        </div>
        {% endif %}
        <form method="POST" action="." accept-charset="UTF-8" role="form">
          {{ form.csrf_token }}
          {{ render_field(form.email, placeholder="Your Email Address",
                                      autofocus="") }}
          {{ render_field(form.password, placeholder="Password") }}
          <div>
          <label>
            <input type="checkbox" name="remember" value="1"> Remember Me
          </label>
          <a role="button" href="">Forgot your password?</a><span class="clearfix"></span>
          </div>
          <button type="submit" name="submit">Sign in</button>
        </form>  
      </div>
    </div>

    참고 :이 템플릿 파일은 데모 용으로 작성된 매우 간단하고 불완전한 예제입니다. Jinja2 문서를 읽고 기본 파일을 사용하여 웹 사이트 서식 파일을 작성하는 것이 좋습니다.

    Step 7: See Your Module In Action

    첫 번째 모듈을 만든 후에 모든 것을 실제로 볼 시간입니다.
    run.py를 사용하여 개발 서버를 실행합니다.

    cd ~/LargeApp
    env/bin/python run.py

    그러면 포트 8080에서 호스팅되는 개발 (즉, 테스트) 서버가 시작됩니다.
    다음 URL로 이동하여 모듈을 방문하십시오.

    http://[your droplet's IP]/auth/signin
    

    로그인 할 수는 없지만 테스트를 위한 데이터를 입력하거나 유효성 검사기를 테스트하여 실제로 로그인 할 수 있습니다.



    아래에 훌륭한 Flask 한글메뉴얼이 존재하며, Flask 를 배우기에 최고의 장소입니다.

    링크 : http://flask-docs-kr.readthedocs.io/ko/latest/index.html


    Deploying with Setuptools


    Setuptools는 Python 라이브러리 및 확장을 배포하는데 일반적으로 사용되는 익스텐션 라이브러리입니다. Python과 함께 제공되는 기본 모듈 설치 시스템인 distutils를 확장하여 더 큰 응용 프로그램을보다 쉽게 배포 할 수있게 해주는 보다 복잡한 여러 구조를 지원합니다.

    • support for dependencies:  라이브러리 또는 응용 프로그램은 자동으로 설치 될 다른 의존 라이브러리의 목록을 선언 할 수 있습니다.
    • package registry:  setuptools는 당신의 패키지를 파이썬 인스톨에 등록합니다. 이렇게 하면 한 패키지가 다른 패키지에서 제공 된 정보를 쿼리 할 수 있습니다. 이 시스템의 가장 잘 알려진 기능은 한 패키지가 다른 패키지를 확장하기 위해 다른 패키지가 후킹 할 수 있는 "진입점"을 선언 할 수있게 해주는 진입점 지원 기능입니다.
    • installation managerpip 다른 라이브러리들을 인스톨 할 수 있습니다.

    python.org에서 Python 2 (> = 2.7.9) 또는 Python 3 (> = 3.4)를 설치했다면 이미 시스템에 pip와 setuptools가있을 것입니다. 그렇지 않으면 직접 설치해야합니다.

    플라스크 자체와 PyPI 에서 찾을 수있는 모든 라이브러리는 setuptools 또는 distutils와 함께 배포 됩니다.

    이 경우에는 응용 프로그램이 yourapplication.py라고 가정하고 모듈을 사용하지 않고 패키지만 사용한다고 가정합니다. 응용 프로그램을 아직 패키지로 변환하지 않았다면 Larger Applications패턴으로 넘어 가서 이것이 어떻게 수행되는지 확인하십시오.

    setuptools를 사용한 작업 배포는 좀 더  복잡하고 자동화된 배포 시나리오를 위한 그저 첫 번째 단계입니다. 프로세스를 완전히 자동화하려면  Deploying with Fabric 장을 읽으십시오.

    기본 Setup 스크립트

    Flask를 설치 했으므로 시스템에서 setuptools를 사용할 수 있습니다. 플라스크는 이미 setuptools에 의존합니다.

    표준 항목으로 . vritualenv 을 사용하는 것이 좋습니다.

    설정(setup)코드는 항상 응용 프로그램 옆에 setup.py라는 파일로 들어갑니다. 파일의 이름은 관례에 불과하지만 모두가 그 이름을 가진 파일을 찾을 것이므로 변경하지 않는 것이 좋습니다.

    Flask 응용 프로그램의 기본 setup.py 파일은 다음과 같습니다.

    from setuptools import setup
    
    setup(
        name='Your Application',
        version='1.0',
        long_description=__doc__,
        packages=['yourapplication'],
        include_package_data=True,
        zip_safe=False,
        install_requires=['Flask']
    )

    서브 패키지를 명시적으로 나열해야한다는 점에 유의하십시오. setuptools가 자동으로 패키지를 검색하도록 하려면 find_packages 함수를 사용할 수 있습니다.

    from setuptools import setup, find_packages
    
    setup(
        ...
        packages=find_packages()
    )

    setup 함수에 대한 대부분의 매개변수는 자체 설명적이어야 하며 include_package_data 및 zip_safe는 아닐 수도 있습니다. include_package_data는 MANIFEST.in 파일을 찾고 패키지데이터와 일치하는 모든 항목을 설치하도록 setuptools에 지시합니다. 이것을 사용하여 정적 파일과 템플릿을 Python 모듈과 함께 배포합니다 (Distributing Resources참조). zip_safe 플래그는 zip 아카이브 생성을 강제하거나 방지하는 데 사용될 수 있습니다. 일반적으로 패키지가 zip 파일로 설치되는 것을 원하지 않는 경우가 있습니다. 일부 도구는 패키지를 지원하지 않으므로 디버깅을 훨씬 더 어렵게 만듭니다.

    Tagging Builds

    릴리스 빌드와 개발 빌드를 구별하는 것이 유용합니다. 이 옵션을 구성하려면 setup.cfg 파일을 추가하십시오.

    [egg_info] tag_build = .dev tag_date = 1

    [aliases] release = egg_info -RDb ”

    python setup.py sdist를 실행하면 ".dev" 및 현재 날짜가 더해진 개발 패키지가 만들어집니다
    : flaskr-1.0.dev20160314.tar.gz.

    python setup.pyrelease sdist를 실행하면 버전 : flaskr-1.0.tar.gz 만있는 릴리스 패키지가 생성됩니다.

    Distributing Resources

    방금 만든 패키지를 설치하려고 하면 static 또는 템플릿과 같은 폴더가 설치되지 않은 것을 알 수 있습니다. 그 이유는 setuptools가 어떤 파일을 추가할 지 모르기 때문입니다. 해야 할 일은 setup.py 파일 옆에 MANIFEST.in 파일을 만드는 것입니다. 이 파일은 tarball에 추가되어야하는 모든 파일을 나열합니다.

    recursive-include yourapplication/templates *
    recursive-include yourapplication/static *

    설치 기능의 include_package_data 매개 변수를 True로 설정하지 않으면 MANIFEST.in 파일에 등록하더라도 사용자를 위해 설치되지 않는다는 사실을 잊지 마십시오!

    Declaring Dependencies

    종속성은 install_requires 매개 변수에서 목록으로 선언됩니다. 이 목록의 각 항목은 설치시 PyPI에서 가져와야하는 패키지의 이름입니다. 기본적으로 항상 최신 버전을 사용하지만 최소 및 최대 버전 요구 사항을 제공 할 수도 있습니다. 여기에 몇 가지 예가 나와 있습니다.

    install_requires=[
        'Flask>=0.2',
        'SQLAlchemy>=0.6',
        'BrokenPackage>=0.7,<=1.0'
    ]

    앞서 언급했듯이, 의존성은 PyPI에서 가져옵니다. PyPI에서 찾을 수 없는 패키지에 의존하고, 다른 사람과 공유하고 싶지 않은 내부 패키지라면? 마치 PyPI 항목이 있는 것처럼 작업하되 setuptools가 tarball을 찾아야 하는 대체 위치 목록을 제공하십시오.

    dependency_links=['http://example.com/yourfiles']

    페이지에 디렉토리 목록이 있고 페이지의 링크가 올바른 파일 이름을 가진 실제 tarball을 가리키고 있는지 확인하십시오. 이것이 setuptools가 파일을 찾는 방법입니다. 패키지가 포함 된 내부 회사 서버가 있는 경우 해당 서버에 대한 URL을 제공하십시오.

    Installing / Developing

    응용 프로그램을 설치하려면 (이상적으로는 virtualenv에) setup.py 스크립트를 install 매개 변수와 함께 실행하십시오. 응용 프로그램을 virtualenv의 site-packages 폴더에 설치하고 모든 종속성을 다운로드하여 설치합니다.

    $ python setup.py install

    패키지를 개발 중이며 요구 사항이 설치되게 하려는 경우 대신  develop 명령을 사용할 수 있습니다.

    $ python setup.py develop

    이렇게하면 데이터를 복사하는 대신 사이트 패키지 폴더에 대한 링크를 설치하는 장점이 있습니다. 각 변경 후에 설치를 다시 실행하지 않고도 코드 작업을 계속할 수 있습니다.


    Angular 2,4 의 경우 아래 링크를 참고하세요.
    https://github.com/ansrivas/angular2-flask



    Flask 와 AngularJS 1.x 웹개발 세팅하기 


    [번역] https://devcereal.com/setting-flask-angularjs/


    AngularJS는 모든 어플리케이션에 사용될 수 있는 강력하고 확장 가능한 Javascript 라이브러리입니다. Flask와 함께 팀을 구성하면 웹 앱을 제작할 수있는 훌륭한 기반을 확보하게 되는데요. Flask와 AngularJS를 시작하는 것은 처음에는 어려운 프로젝트처럼 보일 수 있습니다만 이 가이드를 읽은 후에는 Flask와 AngularJS를 사용하여 기본적인 웹 앱을 만들 수 있을거라 생각합니다.

    이 자습서에서는 Flask가 백엔드 역할을 하고 AngularJS를 프런트 엔드로 사용하여 기본적인 풀스택 응용 프로그램을 설정하는 과정을 안내합니다.

    Download Project Source


    Before We Start

    웹 애플리케이션을 만들기 시작하기 전에 서버에 Flask를  installed Flask 했는지 확인해야합니다. 아직 그렇게 하지 않았다면 지금 먼저하시구요. .또한 이 글은 HTML, 자바 스크립트 및 파이썬에 대한 기본적인 지식이 있다고 가정합니다. 이제는 설정이 필요한 모든 것이 준비되었으므로 애플리케이션을 구축 할 수 있습니다. 렛츠고~!

    Step 1: Project Structure

    Flask와 Angular가 어떻게 통합되어 함께 작동하는지에 대한 개요를 얻기 위해 먼저 프로젝트 구조를 살펴 보겠습니다. 다음은 기본 예제 앱이 완성 될 때의 디렉토리 모습입니다.

    example_project/
        app.py - Our main app
        static/ - Static files
            css/
                style.css
            img/
                angularjs.png
            js/
                app.js - Main angular file
            partials/
                index.html - Homepage partial/template
                about.html - About partial/template
        templates/
            index.html - Basic index template

    이제 새 프로젝트를 만들고 위의 모든 파일과 폴더를 만들어 봅시다.

    App.py

    app.py 모듈은 Flask 백엔드의 두뇌가 될 것입니다. 여기서는 기본 Flask 응용 프로그램과 응용 프로그램 색인 또는 홈페이지의 URL 경로를 정의합니다. Angular에서 모든 URL 라우팅을 수행 할 예정이므로 홈페이지에 정적 템플릿만 제공하면됩니다. 앱 내의 모든 실제 페이지는 부분적으로 유지되며 Angular는 페이지 URL을 기반으로 표시됩니다.

    Static Directory

    CSS, Javascript, 이미지 및 Partials와 같이 제공해야하는 모든 정적 파일은 정적 폴더 안에 보관해야합니다.
    이 파일들은 모두 / static /에 있습니다.

    Partials

    Partials는 각 페이지의 모든 데이터를 보관할 곳입니다. 그것에 대한 좋은 방법은 템플릿 입니다. Angular는 URL 경로를 기반으로 Partials 페이지를 로드하므로 모든 페이지가 단일 페이지에서 게재 될 수 있습니다.

    Template Directory

    템플리트 디렉토리는 모든 정적 템플리트를 보유합니다. 우리의 경우에는 홈페이지에 정적 템플릿만 필요합니다. 이렇게 하면 Angular를 로드하고 Partials 를  표시합니다.

    Step 2: Setting up Flask

    이제 프로젝트의 기본 구조가 완성되었으므로 Flask를 백엔드로 설정하는 작업을 시작할 수 있습니다. 대부분의 URL 라우팅과 데이터를 Angular로 표시하므로 모든 설정을 수행하는 데 많은 코드가 필요하지 않습니다.

    app.py 파일을 열고 다음을 붙여 넣습니다.

    from flask import Flask, send_file
    
    app = Flask(__name__)
    
    @app.route("/")
    def index():
        return send_file("templates/index.html")
    	
    if __name__ == "__main__":
        app.run(host='0.0.0.0')

    보시다시피, 매우 기본적인 basic Flask application.입니다. 그러나 여기엔 중요한 몇 줄의 코드가 있으며 더 자세히 설명 해 보자면

    @app.route("/")
    def index():
        return send_file("templates/index.html")

    Flask에서 URL 라우팅을 정의합니다. 사용자가 홈페이지 ( "/")를 방문하면 index.html 템플릿이 반환됩니다.

    if __name__ == "__main__":
        app.run(host='0.0.0.0')

    언급 할 만한 또 다른 점은 이 예에서 호스트를 0.0.0.0으로 변경했다는 것입니다. 이것은 기본적으로 Flask가 로컬 호스트 또는 127.0.0.1에서 서버를 자동으로 실행하기 때문에 다른 컴퓨터에서 응용 프로그램에 액세스하려는 경우 불가능 합니다. 이 문제를 해결하기 위해 호스트를 웹 서버에 대한 모든 수신 연결을 허용하는 0.0.0.0으로 설정하였지요. 

    Step 3: Adding an Index Template

    이제 Flask 백엔드가 완료되었으며 방문을 시도 할 경우 정적 색인 템플릿이 제공됩니다.그러나 우리가 정적 템플릿을 가지고 있지 않기 때문에 별로 유용하지 않습니다. 그래서 우리는 하나 만들어야합니다. 템플릿 디렉토리로 이동하여 index.html을 열거 나 만들고 나서 다음을 붙여 넣습니다.

    <!doctype html>
    <html>
        <head>
            <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
    		<script src="http://code.angularjs.org/1.5.3/angular-route.min.js"></script>
    		<script src="/static/js/app.js?x49983"></script>
    		<link rel="stylesheet" href="/static/css/style.css?x49983" />
        </head>
        <body ng-app="myApp">
    				
    				<img src="static/img/angularjs.png" />
    				
    				<hr/>
    			 
    			 <div ng-view></div>
        </body>
    </html>

    이것은 기본 템플릿이 될 것입니다. 우리에게 필요한 것은 Angular를 초기화하고 페이지를 로드 할 수있게 하는 것입니다. 이제 우리는 페이지를 로드 할 수 있습니다. 이 페이지는partials으로 저장되고 로드됩니다. 전체 앱이 이 단일 페이지에서 실행된다는 점에 유의할 필요가 있습니다. 따라서 필요한 모든 CSS 또는 JS 파일을 여기서 호출해야합니다.

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
    <script src="http://code.angularjs.org/1.5.3/angular-route.min.js"></script>

    AngularJS와 Angular-Route를 호출하는 작업을 통해 프론트 엔드는 막강해 질 것 입니다.

    <script src="/static/js/app.js?x49983"></script>

    커스텀 스크립트를 로드하십시오.이 스크립트는 Angular 프런트 엔드의 두뇌가 될 것입니다. URL에 / static / 접두어를 붙이십시오. Flask는 정적 디렉토리 만 공개적으로 제공합니다.

    <body ng-app="myApp">

    ng-app를 앵귤러 앱의 이름으로 선언하는 것이 중요합니다. 나중에 app.js 파일에서 이것을 선언해야하므로 오타를 주의 깊게 관찰하십시오!

    <div ng-view></div>

    ng-view  태그가 div 를 정의하였습니다. Angular 는 partials 를 로드 하기 위해 그것을 이용합니다. 이것은 후에 app.js 파일에 정의 할  URL 라우팅에 기반하여 바꿀 것입니다. 

    Step 4: Setting up Angular

    Flask 백엔드 및 기본 템플릿이 모두 설정되었으므로 이제 Angular 코드 작업을 시작 할 시간입니다. 이것은 URL 라우팅을 처리하고 URL을 기반으로 앱 내에 올바른 페이지를 표시합니다. App.js 파일을 열고 다음을 붙여 넣습니다.

    'use strict';   // See note about 'use strict'; below
    
    var myApp = angular.module('myApp', [
     'ngRoute',
    ]);
    
    myApp.config(['$routeProvider',
         function($routeProvider) {
             $routeProvider.
                 when('/', {
                     templateUrl: '/static/partials/index.html',
                 }).
                 when('/about', {
                     templateUrl: '../static/partials/about.html',
                 }).
                 otherwise({
                     redirectTo: '/'
                 });
        }]);

    파일의 맨 위에 'use strict'를 추가 한 이유에 대해 배우고 싶다면 StackOverflow에 대한 유용한 답변이 great answer 있습니다.

    var myApp = angular.module('myApp', [
     'ngRoute',
    ]);

    angular 앱 프로그램 이름을 angular module.로 정의하는 것입니다. 여기에서 사용하는 앱 이름이 이전에 생성 한 index.html 템플릿에서 정의한 앱 이름과 동일한 지 확인하십시오.

    myApp.config(['$routeProvider',
         function($routeProvider) {
             $routeProvider.
                 when('/', {
                     templateUrl: '/static/partials/index.html',
                 }).
                 when('/about', {
                     templateUrl: '../static/partials/about.html',
                 }).
                 otherwise({
                     redirectTo: '/'
                 });
        }]);

    그런 다음 애플리케이션 내에서 URL 경로를 선언합니다. Angular가 이를 처리하여 사용자가 about 페이지 (/ about)를 방문하면 about.html 부분을 반환합니다. 마지막으로 홈페이지로 리다이렉션 한다고 가정합니다. 이렇게 하면 깨진 링크가 있거나 방문객이 잘못된 URL을 사용하는 경우 404 또는 깨진 앱을 반환하는 대신 앱의 홈페이지로 리다이렉션됩니다.

    Step 5: Creating Partials

    Flask 백엔드를 설정 했으므로 인덱스 템플리트를 만들었으며 Angular를 설정했습니다. 이제는 partial 부분에 집중해야 할 때입니다. 예제 웹 앱은 단일 페이지에서 실행되기 때문에 Angular 프런트 엔드는 방문자가 요청하는 URL을 처리 한 다음 해당 페이지의 부분 URL을 반환합니다. partial 템플릿은 인덱스 템플릿에서 정의한 ng-view div 안에 표시됩니다. partial 은 원하는 만큼 간단하거나 복잡해 질 수 있습니다. 정적인 HTML 일 수도 있고 Angular를 사용하여 실시간으로 내용을 표시 할 수도 있습니다.

    / static / partials 디렉토리의 index.html 파일을 열어 다음과 같이 붙여 넣습니다.

    <h1>Home Page</h1>
    
    <p>You should head over to the <a href="/#/about">about page</a>.</p>
    
    <h2>Here's an Angular Demo:</h2>
    
    <div>
        <input type="text" ng-model="yourName" placeholder="Enter a name here">
        <hr>
        <h1>Hello {{ yourName }}</h1>
    </div>

    이 예제에서는 기본 앵귤러 데모를 포함시켜 앵귤러 구성 요소가  partials 내에서 표시되는 방법을 보여줍니다. 또한 멋지고 단순한 페이지를 추가 할 것입니다.

    google로 이동하여 about.html 파일에 다음 내용을 붙여 넣으십시오.

    <h1>About</h1>
    
    <p>Hmm, not much here.</p>
    <p>Best get back to the <a href="/#/">home page</a>.</p>
    
    <p>Tip: going to an incorrect URL will take you to the homepage, <a href="/#/404">Try it!</a></p>

    이번에는 앱 내의 다른 페이지에 대한 링크가있는 페이지를 간단하게 만들었습니다.

    Linking

    아마 색인에 포함 된 링크와 페이지에 관한 링크가 조금 이상하다는 것을 느꼈을 것입니다.

    <a href="/#/">home page</a>

    그렇다면 각 URL 앞에는 / # / 접두어는 무엇일까요? 이것은 단일 페이지 웹 응용 프로그램으로 작성한 모든 링크는 Angular의 URL 라우팅에 의해 선택되도록 해당 웹 페이지 내에 있어야 합니다. 이것은 우리가 실제로 페이지를 떠나는 것이 아니기 때문에 각 URL에 / # / 접두사를 추가하는 이유입니다.

    연결하지 않고 연결을 시도하면 404 오류 또는 내부 서버 오류가 발생합니다.

    Step 6: Testing Your Web App

    이제 거의 끝났습니다. 지금해야 할 일은 응용 프로그램을 테스트하는 것입니다. 명령 프롬프트로 가서 앱을 실행하십시오.

    cd /directory-where-you-saved-the-app
    python app.py

    Flask는 실행 중이며 들어오는 연결을 받아 들일 준비가되었음을 알려줍니다.

    flask angularjs test console

    브라우저에서 서버 IP를 실행하면 완벽하게 작동하는 단일 페이지 웹 응용 프로그램이 확인 할 수 있습니다.

    flask angularjs test page

    Flask와 AngularJS를 사용하여 아주 기본적인 웹 앱을 설치하는 데 필요한 것은이 모두입니다.

    https://www.slideshare.net/aksmj/flask-redis-retrofit?qid=3da6241a-9684-4875-8ac6-64aed4b7413f&v=&b=&from_search=4

    + Recent posts