├── README.md ├── app.py ├── index.html └── OneList.py /README.md: -------------------------------------------------------------------------------- 1 | # 开始使用 (测试版,更新中...) 2 | ## 通过下面URL登录 (右键新标签打开) 3 | [https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa&response_type=code&redirect_uri=http://localhost/onedrive-login&response_mode=query&scope=offline_access%20User.Read%20Files.ReadWrite.All](https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa&response_type=code&redirect_uri=http://localhost/onedrive-login&response_mode=query&scope=offline_access%20User.Read%20Files.ReadWrite.All) 4 | 5 | ## 初始化配置文件 6 | ``` 7 | # 运行 8 | python3 OneList.py 9 | 10 | # 在浏览器地址栏中获取 code 字段内容 11 | # 粘贴并按回车, 每个 code 只能用一次 12 | # 此操作将会自动初始化的配置文件 13 | ``` 14 | 15 | ## 自定义配置文件 16 | ``` 17 | # config.json 18 | 19 | { 20 | // OneDrive 中的某个需要列出的目录 21 | "RootPath": "/Document", 22 | // 网址中的子路径 23 | "SubPath": "/onedrive", 24 | // 目录刷新时间 25 | "FolderRefresh": 900, 26 | // 下载链接刷新时间 27 | "FileRefresh": 1200, 28 | // 认证令牌, 将会自动更新, 保持默认 29 | "RefreshToken": "", 30 | // 这个不用管, 保持默认 31 | "RedirectUri": "http://localhost/onedrive-login" 32 | } 33 | ``` 34 | 35 | ## 运行 36 | ``` 37 | python3 app.py 38 | 39 | # 默认监听 127.0.0.1:5288 , 可在 app.py 中自行更改. 40 | ``` 41 | 42 | ## 展示 43 | [https://moeclub.org/onedrive/](https://moeclub.org/onedrive/) 44 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | # Author: MoeClub.org 4 | 5 | import os 6 | import json 7 | import tornado.web 8 | import tornado.gen 9 | import tornado.ioloop 10 | import tornado.options 11 | import tornado.template 12 | import tornado.httpserver 13 | from threading import Timer, Event 14 | from OneList import OneDrive, Config 15 | 16 | config = Config.load() 17 | MS = OneDrive(config['RefreshToken'], config["RootPath"], config["SubPath"], config["FileRefresh"], config["FolderRefresh"], config["RedirectUri"]) 18 | 19 | 20 | class Handler(tornado.web.RequestHandler): 21 | def realAddress(self): 22 | if 'X-Real-IP' in self.request.headers: 23 | self.request.remote_ip = self.request.headers['X-Real-IP'] 24 | return self.request.remote_ip 25 | 26 | def writeString(self, obj): 27 | if isinstance(obj, (str, int)): 28 | return obj 29 | elif isinstance(obj, (dict, list)): 30 | return json.dumps(obj, ensure_ascii=False) 31 | else: 32 | return obj 33 | 34 | def getPath(self, Path): 35 | Path = str(Path).strip("/").strip() 36 | Root = str(MS.RootPath).strip("/").strip() 37 | Sub = str(MS.SubPath).strip("/").strip() 38 | if str(Path).find(Sub) == 0: 39 | Path = str(Path).replace(Sub, "", 1).strip("/").strip() 40 | return str("/{}/{}").format(Root, Path) 41 | 42 | def currentPath(self, Path): 43 | Path = str(Path).strip("/").strip() 44 | return str("/{}").format(Path) 45 | 46 | @tornado.gen.coroutine 47 | def get(self, Path): 48 | try: 49 | self.realAddress() 50 | items = MS.pageCache(self.getPath(Path)) 51 | if items is None: 52 | raise Exception(str("Error: {}; {};").format(Path, self.getPath(Path))) 53 | if "@link" in items: 54 | self.redirect(items["@link"], permanent=False) 55 | else: 56 | self.render("index.html", currentPath=self.currentPath(Path), rootPath=self.currentPath(MS.SubPath), items=items) 57 | except Exception as e: 58 | print(e) 59 | self.set_status(404) 60 | self.write(self.writeString("No Found")) 61 | self.finish() 62 | 63 | 64 | class Web: 65 | @staticmethod 66 | def main(): 67 | tornado.options.define("host", default='127.0.0.1', help="Host", type=str) 68 | tornado.options.define("port", default=5288, help="Port", type=int) 69 | tornado.options.parse_command_line() 70 | application = tornado.web.Application([(r"/(.*)", Handler)]) 71 | http_server = tornado.httpserver.HTTPServer(application) 72 | http_server.listen(tornado.options.options.port) 73 | tornado.ioloop.IOLoop.instance().start() 74 | 75 | 76 | class LoopRun(Timer): 77 | def __init__(self, interval, function, args=None, kwargs=None): 78 | super(LoopRun, self).__init__(interval, function, args=args, kwargs=kwargs) 79 | self.interval = interval 80 | self.function = function 81 | self.args = args if args is not None else [] 82 | self.kwargs = kwargs if kwargs is not None else {} 83 | self.finished = Event() 84 | 85 | def run(self): 86 | while not self.finished.is_set(): 87 | self.function(*self.args, **self.kwargs) 88 | self.finished.wait(self.interval) 89 | 90 | 91 | class Run: 92 | @staticmethod 93 | def Config(): 94 | MS.getAccessToken() 95 | Config.update(MS) 96 | 97 | @classmethod 98 | def InitMS(cls): 99 | cls.Config() 100 | MS.InCache = False 101 | MS.listItem() 102 | MS.checkCacheTmp() 103 | MS.InCache = True 104 | 105 | @classmethod 106 | def Refresh(cls, interval, function): 107 | RefreshTimer = LoopRun(interval, function) 108 | RefreshTimer.setDaemon(True) 109 | RefreshTimer.start() 110 | 111 | 112 | if __name__ == '__main__': 113 | Run.Refresh(MS.FolderRefresh, Run.InitMS) 114 | Run.Refresh(60, MS.checkFile) 115 | Web.main() 116 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OneList 6 | 107 | 108 | 109 |

110 | OneList 111 |

112 | 113 |
114 |
115 |
116 | 117 |

{{ escape("/" + str(currentPath).replace(rootPath, "", 1).strip("/").strip()) }}

118 |
119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | {% for item in items %} 127 | {% if not str(item).startswith("@") %} 128 | 129 | 137 | 138 | 139 | 140 | {% end %} 141 | {% end %} 142 |
文件修改时间大小
130 | {% if items[item]["@type"] == "file" %} 131 | 132 | {% else %} 133 | 134 | {% end %} 135 | {{ escape(items[item]["name"]) }} 136 | {{ items[item]["date"] }}{{ items[item]["size"] }}
143 |
144 |
145 |
146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /OneList.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | # Author: MoeClub.org 4 | 5 | from urllib import request, error, parse 6 | from threading import Thread 7 | import json 8 | import time 9 | import os 10 | 11 | 12 | class Utils: 13 | sizeUnit = ( 14 | ('B', 2 ** 0), 15 | ('KB', 2 ** 10), 16 | ('MB', 2 ** 20), 17 | ('GB', 2 ** 30), 18 | ('TB', 2 ** 40), 19 | ('PB', 2 ** 50), 20 | ('EB', 2 ** 60), 21 | ('ZB', 2 ** 70), 22 | ('YB', 2 ** 80) 23 | ) 24 | 25 | @classmethod 26 | def getSize(cls, size): 27 | try: 28 | size = int(size) 29 | except: 30 | return "unknown" 31 | for k, v in cls.sizeUnit: 32 | if size <= v * 1024: 33 | return str("{} {}").format(round(size/v, 2), k) 34 | return "unknown" 35 | 36 | @staticmethod 37 | def getTime(t=0): 38 | if t <= 0: 39 | return int(time.time()) 40 | else: 41 | return int(int(time.time()) - t) 42 | 43 | @staticmethod 44 | def formatTime(s="", f="%Y/%m/%d %H:%M:%S"): 45 | try: 46 | assert s 47 | return time.strftime(f, time.strptime(str(s), "%Y-%m-%dT%H:%M:%SZ")) 48 | except: 49 | return str("unknow") 50 | 51 | @staticmethod 52 | def Target(func, args=()): 53 | t = Thread(target=func, args=args) 54 | t.setDaemon(True) 55 | t.start() 56 | 57 | @staticmethod 58 | def http(url, method="GET", headers=None, data=None, coding='utf-8', redirect=True): 59 | method = str(method).strip().upper() 60 | method_allow = ["GET", "HEAD", "POST", "PUT", "DELETE"] 61 | if method not in method_allow: 62 | raise Exception(str("HTTP Method Not Allowed [{}].").format(method)) 63 | 64 | class RedirectHandler(request.HTTPRedirectHandler): 65 | def http_error_302(self, req, fp, code, msg, headers): 66 | pass 67 | 68 | http_error_301 = http_error_303 = http_error_307 = http_error_302 69 | 70 | if headers: 71 | _headers = headers.copy() 72 | else: 73 | _headers = {"User-Agent": "Mozilla/5.0", "Accept-Encoding": ""} 74 | if data is not None and method in ["POST", "PUT"]: 75 | if isinstance(data, (dict, list)): 76 | data = json.dumps(data) 77 | data = str(data).encode(coding) 78 | if 'content-length' not in [str(item).lower() for item in list(_headers.keys())]: 79 | _headers['Content-Length'] = str(len(data)) 80 | else: 81 | data = None 82 | url_obj = request.Request(url, method=method, data=data, headers=_headers) 83 | if redirect: 84 | opener = request.build_opener() 85 | else: 86 | opener = request.build_opener(RedirectHandler) 87 | try: 88 | res_obj = opener.open(url_obj) 89 | except error.HTTPError as err: 90 | res_obj = err 91 | return res_obj 92 | 93 | 94 | class Config: 95 | @staticmethod 96 | def path(): 97 | return os.path.dirname(os.path.abspath(__file__)) 98 | 99 | @staticmethod 100 | def load(file="config.json"): 101 | fd = open(os.path.join(Config.path(), file), "r", encoding="utf-8") 102 | data = fd.read() 103 | fd.close() 104 | return json.loads(data) 105 | 106 | @staticmethod 107 | def update(Obj, file="config.json"): 108 | data = {} 109 | data["RefreshToken"] = Obj.refresh_token 110 | data["FileRefresh"] = Obj.FileRefresh 111 | data["FolderRefresh"] = Obj.FolderRefresh 112 | data["RedirectUri"] = Obj.redirect_uri 113 | data["RootPath"] = Obj.RootPath 114 | data["SubPath"] = Obj.SubPath 115 | fd = open(os.path.join(Config.path(), file), "w", encoding="utf-8") 116 | fd.write(json.dumps(data, ensure_ascii=False, indent=4)) 117 | fd.close() 118 | 119 | @staticmethod 120 | def default(refreshToken, file="config.json"): 121 | data = {} 122 | data["RefreshToken"] = refreshToken 123 | data["FileRefresh"] = 60 * 15 124 | data["FolderRefresh"] = 60 * 12 125 | data["RedirectUri"] = "http://localhost/onedrive-login" 126 | data["RootPath"] = "/" 127 | data["SubPath"] = "" 128 | fd = open(os.path.join(Config.path(), file), "w", encoding="utf-8") 129 | fd.write(json.dumps(data, ensure_ascii=False, indent=4)) 130 | fd.close() 131 | 132 | 133 | class OneDrive: 134 | cache = {} 135 | cacheUrl = {} 136 | cacheRoot = {} 137 | cacheOnce = True 138 | InCache = False 139 | 140 | def __init__(self, refreshToken, rootPath="", subPath="", fileRefresh=60*30, folderRefresh=60*15, redirectUri="http://localhost/onedrive-login"): 141 | self.RootPath = rootPath 142 | self.SubPath = subPath 143 | self.FileRefresh = int(fileRefresh) 144 | self.FolderRefresh = int(folderRefresh) 145 | self.redirect_uri = redirectUri 146 | self.refresh_token = refreshToken 147 | self.access_token = "" 148 | 149 | @staticmethod 150 | def accessData(grantType, redirectUri='http://localhost/onedrive-login'): 151 | return { 152 | 'client_id': 'ea2b36f6-b8ad-40be-bc0f-e5e4a4a7d4fa', 153 | 'client_secret': 'h27zG8pr8BNsLU0JbBh5AOznNS5Of5Y540l/koc7048=', 154 | 'redirect_uri': redirectUri, 155 | 'grant_type': grantType, 156 | "scope": "User.Read Files.ReadWrite.All" 157 | } 158 | 159 | @staticmethod 160 | def drivePath(path): 161 | path = str(path).strip(":").split(":", 1)[-1] 162 | while '//' in path: 163 | path = str(path).replace('//', '/') 164 | while " " in path: 165 | path = str(path).replace(" ", "%20") 166 | while "#" in path: 167 | path = str(path).replace("#", "%23") 168 | if path == "/": 169 | return path 170 | else: 171 | return str(":/{}:").format(str(path).strip('/')) 172 | 173 | @staticmethod 174 | def urlPath(path, hasRoot=False): 175 | pathArray = str(str(path).strip(":").split(":", 1)[-1]).strip("/").split("/") 176 | newPath = str("/").join([parse.unquote(str(item).strip()) for item in pathArray if str(item).strip()]) 177 | if hasRoot: 178 | setRoot = "/drive/root:/" 179 | else: 180 | setRoot = "/" 181 | return str("{}{}").format(setRoot, newPath) 182 | 183 | def findCache(self, path, useCache=0): 184 | path = self.urlPath(path, hasRoot=True) 185 | if useCache == 0: 186 | _cache = self.cacheRoot 187 | else: 188 | _cache = self.cacheUrl 189 | if path not in _cache: 190 | pathArray = str(path).rsplit("/", 1) 191 | try: 192 | return _cache[pathArray[0]][pathArray[1]] 193 | except: 194 | return None 195 | else: 196 | return _cache[path] 197 | 198 | @staticmethod 199 | def getHeader(accessToken=""): 200 | _header = { 201 | 'User-Agent': 'ISV|OneList/1.1', 202 | 'Accept': 'application/json; odata.metadata=none', 203 | } 204 | if accessToken: 205 | _header['Authorization'] = str("Bearer {}").format(accessToken) 206 | return _header 207 | 208 | def getToken(self, respCode): 209 | data = self.accessData('authorization_code') 210 | data["code"] = respCode 211 | Data = "&".join([str("{}={}").format(item, data[item]) for item in data]) 212 | page = Utils.http("https://login.microsoftonline.com/common/oauth2/v2.0/token", "POST", data=Data, headers=self.getHeader()) 213 | resp = json.loads(page.read().decode()) 214 | print(resp) 215 | if "refresh_token" in resp and "access_token" in resp: 216 | self.access_token = resp["access_token"] 217 | self.refresh_token = resp["refresh_token"] 218 | else: 219 | raise Exception("Error, Get refresh token.") 220 | 221 | def getAccessToken(self, refreshToken=None): 222 | data = self.accessData('refresh_token') 223 | if refreshToken is None: 224 | data["refresh_token"] = self.refresh_token 225 | else: 226 | data["refresh_token"] = refreshToken 227 | Data = "&".join([str("{}={}").format(item, data[item]) for item in data]) 228 | page = Utils.http("https://login.microsoftonline.com/common/oauth2/v2.0/token", "POST", data=Data, headers=self.getHeader()) 229 | resp = json.loads(page.read().decode()) 230 | if "refresh_token" in resp and "access_token" in resp: 231 | self.access_token = resp["access_token"] 232 | self.refresh_token = resp["refresh_token"] 233 | else: 234 | raise Exception("Error, Get Access.") 235 | 236 | def listItem(self, path=None): 237 | if path is None: 238 | path = self.RootPath 239 | url = str("https://graph.microsoft.com/v1.0/me/drive/root{}?expand=children($select=name,size,file,folder,parentReference,lastModifiedDateTime)").format(self.drivePath(path)) 240 | print("Cache:", self.urlPath(path)) 241 | page = Utils.http(url, headers=self.getHeader(self.access_token)) 242 | data = json.loads(page.read().decode()) 243 | if "error" in data: 244 | print(data["error"]["message"]) 245 | else: 246 | if "@microsoft.graph.downloadUrl" in data: 247 | self.getItem(data) 248 | else: 249 | self.getFolder(data) 250 | 251 | def getFolder(self, Json): 252 | if "children" in Json: 253 | for item in Json["children"]: 254 | parentKey = str("{}").format(item["parentReference"]["path"]) 255 | if parentKey not in self.cache: 256 | self.cache[parentKey] = {"@time": Utils.getTime()} 257 | self.cache[parentKey][item["name"]] = { 258 | "name": item["name"], 259 | "size": Utils.getSize(item["size"]), 260 | "date": Utils.formatTime(item["lastModifiedDateTime"]), 261 | "@time": Utils.getTime() 262 | } 263 | if "folder" in item: 264 | self.cache[parentKey][item["name"]]["@type"] = "folder" 265 | self.listItem(str("{}/{}").format(parentKey, item["name"])) 266 | elif "file" in item: 267 | self.cache[parentKey][item["name"]]["@type"] = "file" 268 | 269 | def getItem(self, Json): 270 | parentKey = Json["parentReference"]["path"] 271 | if parentKey not in self.cacheUrl: 272 | self.cacheUrl[parentKey] = {} 273 | if Json["name"] not in self.cacheUrl[parentKey]: 274 | self.cacheUrl[parentKey][Json["name"]] = {} 275 | try: 276 | self.cacheRoot[parentKey][Json["name"]]["name"] = Json["name"] 277 | self.cacheRoot[parentKey][Json["name"]]["date"] = Utils.formatTime(Json["lastModifiedDateTime"]) 278 | self.cacheRoot[parentKey][Json["name"]]["size"] = Utils.getSize(Json["size"]) 279 | self.cacheRoot[parentKey][Json["name"]]["@time"] = Utils.getTime() 280 | self.cacheRoot[parentKey][Json["name"]]["@type"] = "file" 281 | except: 282 | print("Cache Error:", str("{}/{}").format(parentKey, Json["name"])) 283 | self.cacheUrl[parentKey][Json["name"]]["@link"] = Json["@microsoft.graph.downloadUrl"] 284 | self.cacheUrl[parentKey][Json["name"]]["@time"] = Utils.getTime() 285 | 286 | def itemCache(self, path, cache): 287 | NotInCache = False 288 | isFolder = True 289 | timeOut = self.FolderRefresh 290 | if "@type" in cache and cache["@type"] == "file": 291 | timeOut = self.FileRefresh 292 | isFolder = False 293 | if not self.findCache(path, 1): 294 | NotInCache = True 295 | if self.InCache: 296 | if NotInCache and not isFolder: 297 | self.listItem(path) 298 | else: 299 | if NotInCache or Utils.getTime(cache['@time']) > timeOut: 300 | self.listItem(path) 301 | return isFolder 302 | 303 | def pageCache(self, path): 304 | path = self.urlPath(path) 305 | cache = self.findCache(path, 0) 306 | if self.cacheOnce and self.cache: 307 | self.checkCacheTmp(False) 308 | if cache: 309 | isFolder = self.itemCache(path, cache) 310 | else: 311 | return None 312 | if isFolder: 313 | return self.findCache(path, 0) 314 | else: 315 | return self.findCache(path, 1) 316 | 317 | def checkCacheTmp(self, clear=True): 318 | if self.cache: 319 | self.cacheRoot = self.cache.copy() 320 | if clear: 321 | if self.cacheOnce: 322 | self.cacheOnce = False 323 | self.cache = {} 324 | 325 | def checkFile(self): 326 | if not self.cacheUrl: 327 | return 328 | tmpCache = self.cacheUrl.copy() 329 | for parentItem in tmpCache: 330 | for Item in tmpCache[parentItem]: 331 | try: 332 | if Utils.getTime(tmpCache[parentItem][Item]['@time']) > self.FileRefresh: 333 | del self.cacheUrl[parentItem][Item] 334 | except: 335 | continue 336 | 337 | 338 | if __name__ == "__main__": 339 | while True: 340 | code = str(input("code:").strip()) 341 | if code: break 342 | ms = OneDrive("") 343 | ms.getToken(code) 344 | Config.default(ms.refresh_token) 345 | print("Success, Init config.") 346 | --------------------------------------------------------------------------------