├── 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 |
112 |
113 |
114 |
115 |
119 |
120 |
121 |
122 | | 文件 |
123 | 修改时间 |
124 | 大小 |
125 |
126 | {% for item in items %}
127 | {% if not str(item).startswith("@") %}
128 |
129 | |
130 | {% if items[item]["@type"] == "file" %}
131 |
132 | {% else %}
133 |
134 | {% end %}
135 | {{ escape(items[item]["name"]) }}
136 | |
137 | {{ items[item]["date"] }} |
138 | {{ items[item]["size"] }} |
139 |
140 | {% end %}
141 | {% end %}
142 |
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 |
--------------------------------------------------------------------------------