├── .gitignore ├── README.MD ├── app.py ├── cache.py ├── config.json.template ├── config.py ├── dcache.py ├── manifest.yml ├── onedrive.py ├── process.py ├── requirements.txt ├── templates └── list.html └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea 3 | config.json 4 | **/.DS_Store 5 | __pycache__ 6 | tmp 7 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # OneList 2 | 3 | 本版本不需要使用Redis! 4 | 5 | ## Installation 6 | 7 | ```bash 8 | # apt install python3-pip redis-server 9 | 10 | git clone https://github.com/0oVicero0/OneList.git 11 | cd OneList 12 | 13 | pip3 install -r requirements.txt 14 | # Get refresh_token --> Setup config 15 | # The in config.json does not need "<" and ">". 16 | 17 | gunicorn app:app -b 127.0.0.1:5000 -D 18 | ``` 19 | 20 | ## Configuration 21 | 22 | Create a config file named `config.json` 23 | 24 | ```json 25 | { 26 | "token": "", 27 | "location_path": "/", 28 | "start_directory": "/", 29 | "threads": 3, 30 | "diff_seconds": 480, 31 | "refresh_seconds": 720, 32 | "metadata_cached_seconds": 768, 33 | "structure_cached_seconds": 840 34 | } 35 | ``` 36 | ## Get refresh_token 37 | 38 | ### Method 1 (Auto, Recommend): 39 | 40 | #### Get refresh_token 41 | 42 | > https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&client_id=ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa&redirect_uri=https://api.moeclub.org/onedrive-login 43 | 44 | ### Method 2 (Manual): 45 | 46 | #### Get auth_token in url 47 | 48 | > https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&client_id=ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa&redirect_uri=http://localhost/onedrive-login 49 | 50 | #### Get refresh_token 51 | 52 | ```bash 53 | code="" 54 | wget --no-check-certificate --post-data="client_id=ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa&client_secret=h27zG8pr8BNsLU0JbBh5AOznNS5Of5Y540l/koc7048=&grant_type=authorization_code&resource=https://api.office.com/discovery/&redirect_uri=http://localhost/onedrive-login&code=$code" 'https://login.microsoftonline.com/common/oauth2/token' -qO- 55 | ``` 56 | 57 | ## Demo 58 | > https://moeclub.org/onedrive 59 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | # Author: MoeClub.org, sxyazi 4 | 5 | from process import od 6 | from config import config 7 | from utils import path_format 8 | from flask import Flask, abort, redirect, render_template, Blueprint 9 | 10 | bp = Blueprint('main', __name__, url_prefix=config.location_path) 11 | 12 | 13 | # Views 14 | @bp.route('/favicon.ico') 15 | def favicon(): 16 | return abort(404) 17 | 18 | 19 | @bp.route('/', defaults={'path': '/'}) 20 | @bp.route('/') 21 | def catch_all(path): 22 | info = od.list_items_with_cache( 23 | path_format(config.start_directory + '/' + path)) 24 | 25 | if info.is_file: # download 26 | return redirect(info.files[0]['download_url']) 27 | 28 | return render_template('list.html', info=info, path=path_format(path).strip('/')) 29 | 30 | 31 | # Filters 32 | @bp.app_template_filter('date_format') 33 | def date_format(str, format='%Y/%m/%d %H:%M:%S'): 34 | from dateutil import tz 35 | from datetime import datetime 36 | 37 | dt = datetime.strptime(str, "%Y-%m-%dT%H:%M:%SZ") 38 | return dt.replace(tzinfo=tz.tzutc()).astimezone(tz.gettz('Asia/Shanghai')).strftime(format) 39 | 40 | 41 | @bp.app_template_filter('file_size') 42 | def file_size(size): 43 | unit = ( 44 | ('B', 2**0), 45 | ('KB', 2**10), 46 | ('MB', 2**20), 47 | ('GB', 2**30), 48 | ('TB', 2**40), 49 | ('PB', 2**50), 50 | ('EB', 2**60), 51 | ('ZB', 2**70), 52 | ('YB', 2**80) 53 | ) 54 | 55 | for k, v in unit: 56 | if size <= v * 1024: 57 | return '%s %s' % (round(size/v, 2), k) 58 | return 'unknown' 59 | 60 | 61 | app = Flask(__name__) 62 | app.register_blueprint(bp) 63 | 64 | if __name__ == '__main__': 65 | app.run(host='0.0.0.0', port='8080', debug=False) 66 | -------------------------------------------------------------------------------- /cache.py: -------------------------------------------------------------------------------- 1 | # Author: MoeClub.org, sxyazi 2 | 3 | import redis 4 | import pickle 5 | import hashlib 6 | 7 | r = redis.Redis(host='127.0.0.1', port=6379, db=0) 8 | # r = redis.Redis(host='Redis_URL', port=PORT, db=0, password='Password', ssl=True, ssl_ca_certs='ca.pem') 9 | 10 | 11 | class Cache: 12 | CACHED_SECONDS = 768 13 | 14 | @classmethod 15 | def get(cls, path): 16 | if cls.has(path): 17 | return pickle.loads(r.get(cls._get_key(path))) 18 | return False 19 | 20 | @classmethod 21 | def has(cls, path): 22 | return r.exists(cls._get_key(path)) 23 | 24 | @classmethod 25 | def set(cls, path, entity, expire=CACHED_SECONDS): 26 | return r.set(cls._get_key(path), pickle.dumps(entity), expire) 27 | 28 | @classmethod 29 | def rem(cls, path): 30 | return r.delete(cls._get_key(path)) 31 | 32 | @staticmethod 33 | def _get_key(path): 34 | return 'onelist:' + hashlib.md5(path.encode()).hexdigest() 35 | -------------------------------------------------------------------------------- /config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "token":"", 3 | "location_path": "/", 4 | "start_directory": "/", 5 | "threads": 3, 6 | "diff_seconds": 480, 7 | "refresh_seconds": 720, 8 | "metadata_cached_seconds": 768, 9 | "structure_cached_seconds": 840 10 | } 11 | 12 | 13 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class Config: 5 | def __init__(self, opts={}): 6 | self._options = opts 7 | 8 | def __str__(self): 9 | return json.dumps(self._options, indent=2) 10 | 11 | def __getattr__(self, name): 12 | if name in self._options: 13 | if isinstance(self._options[name], dict): 14 | return Config(self._options[name]) 15 | else: 16 | return self._options[name] 17 | 18 | return None 19 | 20 | 21 | def _parse_config(): 22 | try: 23 | with open('config.json', 'r', encoding='utf-8') as f: 24 | return json.loads(f.read()) 25 | except: 26 | return {} 27 | 28 | 29 | config = Config(_parse_config()) 30 | -------------------------------------------------------------------------------- /dcache.py: -------------------------------------------------------------------------------- 1 | import diskcache 2 | import pickle 3 | import hashlib 4 | 5 | 6 | r = diskcache.Cache('tmp') 7 | 8 | class Cache: 9 | CACHED_SECONDS = 768 10 | 11 | @classmethod 12 | def get(cls, path): 13 | if cls.has(path): 14 | return pickle.loads(r.get(cls._get_key(path))) 15 | return False 16 | 17 | @classmethod 18 | def has(cls, path): 19 | return r.get(cls._get_key(path)) is not None 20 | 21 | @classmethod 22 | def set(cls, path, entity, expire=CACHED_SECONDS): 23 | return r.set(cls._get_key(path), pickle.dumps(entity), expire) 24 | 25 | @classmethod 26 | def rem(cls, path): 27 | return r.delete(cls._get_key(path)) 28 | 29 | @staticmethod 30 | def _get_key(path): 31 | return 'onelist:' + hashlib.md5(path.encode()).hexdigest() 32 | -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: ruyonet 4 | command: python app.py 5 | -------------------------------------------------------------------------------- /onedrive.py: -------------------------------------------------------------------------------- 1 | # Author: MoeClub.org, sxyazi 2 | 3 | import json 4 | import pickle 5 | import hashlib 6 | 7 | from dcache import Cache 8 | from utils import path_format 9 | from config import config 10 | from urllib import request, parse 11 | 12 | 13 | class _ItemInfo: 14 | def __init__(self): 15 | self.files = [] 16 | self.folders = [] 17 | self.is_file = False 18 | 19 | 20 | class OneDrive(): 21 | _request_headers = {'User-Agent': 'ISV|MoeClub|OneList/1.0', 22 | 'Accept': 'application/json; odata.metadata=none'} 23 | 24 | def __init__(self): 25 | self.api_url = '' 26 | self.resource_id = '' 27 | 28 | self.expires_on = '' 29 | self.access_token = '' 30 | self.refresh_token = config.token 31 | 32 | def get_access(self, resource='https://api.office.com/discovery/'): 33 | res = self._http_request('https://login.microsoftonline.com/common/oauth2/token', method='POST', data={ 34 | 'client_id': 'ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa', 35 | 'client_secret': 'h27zG8pr8BNsLU0JbBh5AOznNS5Of5Y540l/koc7048=', 36 | 'redirect_uri': 'http://localhost/onedrive-login', 37 | 'refresh_token': self.refresh_token, 38 | 'grant_type': 'refresh_token', 39 | 'resource': resource 40 | }) 41 | 42 | self.expires_on = res['expires_on'] 43 | self.access_token = res['access_token'] 44 | self.refresh_token = res['refresh_token'] 45 | 46 | if not self.access_token: 47 | print('Unauthorized') 48 | exit(1) 49 | 50 | def get_resource(self): 51 | res = self._http_request( 52 | 'https://api.office.com/discovery/v2.0/me/services') 53 | 54 | for item in res['value']: 55 | if item['serviceApiVersion'] == 'v2.0': 56 | self.api_url = item['serviceEndpointUri'] 57 | self.resource_id = item['serviceResourceId'] 58 | 59 | if not self.api_url: 60 | raise Exception('Failed to get api url') 61 | 62 | def list_items(self, path='/'): 63 | url = '%s/drive/root:%s/?expand=children(select=name,size,file,folder,parentReference,lastModifiedDateTime)' % ( 64 | self.api_url, parse.quote(path_format(path))) 65 | res = self._http_request(url) 66 | 67 | info = _ItemInfo() 68 | self._append_item(info, res) 69 | 70 | if 'children' in res: 71 | for children in res['children']: 72 | self._append_item(info, children) 73 | 74 | if info.files and not info.folders: 75 | info.is_file = True 76 | return info 77 | 78 | def list_all_items(self, path='/'): 79 | ret = _ItemInfo() 80 | tasks = [{'full_path': path}] 81 | 82 | while len(tasks) > 0: 83 | c = tasks.pop(0) 84 | 85 | tmp = self.list_items(c['full_path']) 86 | tasks += tmp.folders[1:] 87 | 88 | ret.files += tmp.files 89 | ret.folders += tmp.folders[1:] 90 | 91 | if ret.files and not ret.folders: 92 | ret.is_file = True 93 | return ret 94 | 95 | def list_items_with_cache(self, path='/', flash=False): 96 | path = path_format(path) 97 | key = ('tmp:' + path) if flash else path 98 | 99 | if not Cache.has(key): 100 | if flash: 101 | Cache.set(key, self.list_items(path), 10) 102 | else: 103 | print('missing: %s' % path) 104 | 105 | info = self.list_items(path) 106 | if info.is_file: 107 | Cache.set(key, info, config.metadata_cached_seconds) 108 | else: 109 | Cache.set(key, info, config.structure_cached_seconds) 110 | 111 | return Cache.get(key) 112 | 113 | def _http_request(self, url, method='GET', data={}): 114 | headers = self._request_headers.copy() 115 | if self.access_token: 116 | headers['Authorization'] = "Bearer " + self.access_token 117 | data = parse.urlencode(data).encode('utf-8') 118 | res = json.loads(request.urlopen(request.Request( 119 | url, method=method, data=data, headers=headers)).read().decode('utf-8')) 120 | 121 | if 'error' in res: 122 | raise Exception(res['error']['message']) 123 | return res 124 | 125 | def _append_item(self, info, item): 126 | if 'path' not in item['parentReference']: 127 | path = item['name'] = '/' 128 | else: 129 | path = item['parentReference']['path'][12:] or '/' 130 | 131 | dic = { 132 | 'name': item['name'], 133 | 'size': item['size'], 134 | 'hash': self._get_item_hash(item), 135 | 'folder': path, 136 | 'full_path': path_format(path + '/' + item['name']), 137 | 'updated_at': item['lastModifiedDateTime'] 138 | } 139 | if '@content.downloadUrl' in item: 140 | dic['download_url'] = item['@content.downloadUrl'] 141 | 142 | if 'file' in item: 143 | info.files.append(dic) 144 | else: 145 | info.folders.append(dic) 146 | 147 | def _get_item_hash(self, item): 148 | dic = { 149 | 'name': item['name'], 150 | 'size': item['size'], 151 | 'parentReference': item['parentReference'], 152 | 'lastModifiedDateTime': item['lastModifiedDateTime'] 153 | } 154 | 155 | if 'file' in item: 156 | dic['file'] = item['file'] 157 | else: 158 | dic['folder'] = item['folder'] 159 | 160 | return hashlib.md5(pickle.dumps(dic)).hexdigest() 161 | -------------------------------------------------------------------------------- /process.py: -------------------------------------------------------------------------------- 1 | import time 2 | import schedule 3 | import threading 4 | from dcache import Cache 5 | from config import config 6 | from onedrive import OneDrive 7 | from utils import path_format 8 | 9 | 10 | od = OneDrive() 11 | 12 | 13 | class Process: 14 | tasks = [] 15 | 16 | @staticmethod 17 | def runner(): 18 | while True: 19 | schedule.run_pending() 20 | time.sleep(1) 21 | 22 | @staticmethod 23 | def refresh_token(): 24 | od.get_access() 25 | od.get_resource() 26 | od.get_access(od.resource_id) 27 | 28 | @classmethod 29 | def refresh_difference(cls): 30 | cls.tasks.append({'full_path': config.start_directory}) 31 | 32 | @classmethod 33 | def worker(cls): 34 | while True: 35 | if len(cls.tasks) < 1: 36 | time.sleep(.1) 37 | continue 38 | 39 | c = cls.tasks.pop(0) 40 | info = od.list_items_with_cache(c['full_path'], True) 41 | 42 | for f in info.files: 43 | p = f['full_path'] 44 | 45 | if not Cache.has(p): 46 | continue 47 | 48 | file = Cache.get(p).files[0] 49 | if file['hash'] != f['hash']: 50 | print('expired file: %s' % p) 51 | Cache.rem(p) 52 | 53 | for f in info.folders: 54 | p = f['full_path'] 55 | 56 | if not Cache.has(p): 57 | print('no cached: %s' % p) 58 | new = od.list_items_with_cache(p, True) 59 | 60 | cls.cache_all(new) 61 | cls.tasks += new.folders[1:] 62 | continue 63 | 64 | folder = Cache.get(p).folders[0] 65 | if folder['hash'] != f['hash']: 66 | print('expired folder: %s' % p) 67 | new = od.list_items_with_cache(p, True) 68 | 69 | cls.cache_all(new) 70 | cls.tasks += new.folders[1:] 71 | 72 | @staticmethod 73 | def cache_all(info): 74 | for f in info.folders: 75 | Cache.set(f['full_path'], od.list_items_with_cache( 76 | f['full_path'], True), config.structure_cached_seconds) 77 | 78 | 79 | Process.refresh_token() 80 | Process.refresh_difference() 81 | 82 | schedule.every(config.refresh_seconds).seconds.do(Process.refresh_token) 83 | schedule.every(config.diff_seconds).seconds.do(Process.refresh_difference) 84 | 85 | threading.Thread(target=Process.runner).start() 86 | for _ in range(config.threads): 87 | threading.Thread(target=Process.worker).start() 88 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | schedule==0.6.0 3 | gunicorn==19.9.0 4 | python-dateutil==2.8.0 5 | diskcache==3.1.1 6 | -------------------------------------------------------------------------------- /templates/list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OneList 6 | 107 | 108 | 109 | 110 |

111 | OneList 112 |

113 | 114 |
115 |
116 |
117 | {% if path%} 118 | 119 | 120 | 121 | {% endif %} 122 |

{{ path or '/' }}

123 |
124 |
125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {% for v in info.folders[1:] %} 133 | 134 | 137 | 138 | 139 | 140 | {% endfor %} 141 | 142 | {% for v in info.files %} 143 | 144 | 147 | 148 | 149 | 150 | {% endfor %} 151 |
文件修改时间大小
135 | {{ v.name }} 136 | {{ v.updated_at | date_format }}{{ v.size | file_size }}
145 | {{ v.name }} 146 | {{ v.updated_at | date_format }}{{ v.size | file_size }}
152 |
153 |
154 |
155 | 156 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import hashlib 3 | from datetime import datetime 4 | 5 | 6 | def path_format(path): 7 | while '//' in path: 8 | path = path.replace('//', '/') 9 | 10 | return '/' + path.strip('/') 11 | --------------------------------------------------------------------------------