From b7a0f46685491c44a3a0bf50635c3edfc4fc94d9 Mon Sep 17 00:00:00 2001 From: Nathan Chen Date: Wed, 11 Dec 2019 17:10:21 -0500 Subject: [PATCH] Collect filesystem initial commit This tool is used for storing large collect files when reporting bugs via Launchpad. Simply use the tool to upload a file and then add its URL location into the Launchpad bug. You no longer need to split up large collect files and attach multiple files to your bugs. This tool is designed using Flask Web Framework, meanwhile docker is used for easy deployment. The directory /app holds the Flask application, /db contains the script for database initialization. Bash file start.sh includes the commands used for the first time docker deployment. The file update.sh let developers push their changes to the docker repository. Change the repository's name and tag accordingly, in this case it is nchen1windriver/collect and demo. A config file was added for security purpose in this commit. Change-Id: I192c3fca541f99773e0395418a9f11e01c27a5a7 Signed-off-by: Nathan Chen --- .gitignore | 1 + tools/collect_filesystem/Dockerfile | 18 + tools/collect_filesystem/app/app.py | 842 ++++++++++++++++++ tools/collect_filesystem/app/requirements.txt | 2 + tools/collect_filesystem/app/static/main.js | 415 +++++++++ .../collect_filesystem/app/static/openid.png | Bin 0 -> 433 bytes tools/collect_filesystem/app/static/style.css | 46 + .../collect_filesystem/app/templates/413.html | 7 + .../app/templates/_confirm.html | 7 + .../app/templates/bak/upload.html | 51 ++ .../app/templates/create_profile.html | 28 + .../app/templates/edit_file.html | 27 + .../app/templates/edit_profile.html | 22 + .../app/templates/index.html | 10 + .../app/templates/launchpad.html | 40 + .../app/templates/launchpads.html | 44 + .../app/templates/layout.html | 28 + .../app/templates/login.html | 18 + .../app/templates/public_files.html | 64 ++ .../app/templates/upload.html | 58 ++ .../app/templates/user_files.html | 72 ++ tools/collect_filesystem/db/init.sql | 7 + tools/collect_filesystem/docker-compose.yml | 19 + tools/collect_filesystem/setup package.zip | Bin 0 -> 23119 bytes tools/collect_filesystem/start.sh | 3 + tools/collect_filesystem/update.sh | 3 + 26 files changed, 1832 insertions(+) create mode 100644 tools/collect_filesystem/Dockerfile create mode 100644 tools/collect_filesystem/app/app.py create mode 100644 tools/collect_filesystem/app/requirements.txt create mode 100644 tools/collect_filesystem/app/static/main.js create mode 100644 tools/collect_filesystem/app/static/openid.png create mode 100644 tools/collect_filesystem/app/static/style.css create mode 100644 tools/collect_filesystem/app/templates/413.html create mode 100644 tools/collect_filesystem/app/templates/_confirm.html create mode 100644 tools/collect_filesystem/app/templates/bak/upload.html create mode 100644 tools/collect_filesystem/app/templates/create_profile.html create mode 100644 tools/collect_filesystem/app/templates/edit_file.html create mode 100644 tools/collect_filesystem/app/templates/edit_profile.html create mode 100644 tools/collect_filesystem/app/templates/index.html create mode 100644 tools/collect_filesystem/app/templates/launchpad.html create mode 100644 tools/collect_filesystem/app/templates/launchpads.html create mode 100644 tools/collect_filesystem/app/templates/layout.html create mode 100644 tools/collect_filesystem/app/templates/login.html create mode 100644 tools/collect_filesystem/app/templates/public_files.html create mode 100644 tools/collect_filesystem/app/templates/upload.html create mode 100644 tools/collect_filesystem/app/templates/user_files.html create mode 100644 tools/collect_filesystem/db/init.sql create mode 100644 tools/collect_filesystem/docker-compose.yml create mode 100644 tools/collect_filesystem/setup package.zip create mode 100644 tools/collect_filesystem/start.sh create mode 100644 tools/collect_filesystem/update.sh diff --git a/.gitignore b/.gitignore index b3651d72..30984c31 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .tox *.egg-info *.swp +.idea diff --git a/tools/collect_filesystem/Dockerfile b/tools/collect_filesystem/Dockerfile new file mode 100644 index 00000000..4655fdee --- /dev/null +++ b/tools/collect_filesystem/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:18.04 + +RUN apt-get update -y && \ + apt-get install -y python3-dev python3-pip libffi-dev + +RUN pip3 install --upgrade pip + +RUN pip3 install python-magic flask_mail flask_openid werkzeug pymysql launchpadlib apscheduler + +WORKDIR /app + +COPY app/ /app/ + +RUN pip3 install -r requirements.txt + +EXPOSE 5000:5000 + +CMD ["python3", "app.py"] diff --git a/tools/collect_filesystem/app/app.py b/tools/collect_filesystem/app/app.py new file mode 100644 index 00000000..c46ef569 --- /dev/null +++ b/tools/collect_filesystem/app/app.py @@ -0,0 +1,842 @@ +import os +import shutil +import logging +import zipfile +from datetime import datetime +from datetime import timedelta +from functools import wraps +import tarfile +from mail_config import custom_mail_password +from mail_config import custom_mail_server +from mail_config import custom_mail_username +from mail_config import custom_server_admins + +import magic +from flask_mail import Mail +from flask_mail import Message +from urllib.parse import quote +from urllib.parse import unquote + +from flask import Flask +from flask import flash +from flask import request +from flask import redirect +from flask import render_template +from flask import g +from flask import session +from flask import url_for +from flask import abort +from flask import send_file +from flask import make_response +from flask import jsonify +from flask import after_this_request +import flask_openid +from openid.extensions import pape +from werkzeug.exceptions import RequestEntityTooLarge +from werkzeug.utils import secure_filename +import pymysql.cursors +from launchpadlib.launchpad import Launchpad +import atexit + +from apscheduler.schedulers.background import BackgroundScheduler + +logging.basicConfig(filename='collect.log', level=logging.INFO, format='%(asctime)s:%(levelname)s:%(message)s') + +app = Flask(__name__) +app.config['BASE_DIR'] = os.path.abspath( + os.path.dirname(__file__) +) +app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 * 1000 +app.testing = False +app.config.update( + # Gmail sender settings + MAIL_SERVER=custom_mail_server, + MAIL_PORT=465, + MAIL_USE_TLS=False, + MAIL_USE_SSL=True, + MAIL_USERNAME=custom_mail_username, + MAIL_PASSWORD=custom_mail_password, + MAIL_DEFAULT_SENDER=custom_mail_username, + SECRET_KEY='development key', + DEBUG=True +) + +mail = Mail(app) + +ALLOWED_EXTENSIONS = set(['log', 'txt', 'zip', 'tar', 'tgz']) +DISALLOWED_EXTENSIONS = set(['exe', 'mp4', 'avi', 'mkv']) +ALLOWED_MIME_TYPES = set(['text/plain', 'application/x-bzip2', 'application/zip', 'application/x-gzip', + 'application/x-tar']) +DISALLOWED_MIME_TYPES = set(['application/x-dosexec', 'application/x-msdownload']) +TARGETED_TAR_CONTENTS = set(['controller', 'storage', 'compute']) +SERVER_ADMINS = custom_server_admins +THRESHOLD = 0.8 + +oid = flask_openid.OpenID(app, safe_roots=[], extension_responses=[pape.Response]) + + +def delete_old_files(): + time_before = datetime.now() - timedelta(months=6) + with connect().cursor() as cursor: + files_sql = "SELECT name, user_id, launchpad_id FROM files WHERE modified_date<%s;" + cursor.execute(files_sql, (time_before,)) + files = cursor.fetchall() + for file in files: + os.remove(os.path.join(app.config['BASE_DIR'], 'files', file['user_id'], str(file['launchpad_id']), + file['name'])) + logging.info('Outdated file deleted: {}/{}/{}'.format(file['user_id'], file['launchpad_id'], file['name'])) + + sql = "DELETE FROM files WHERE modified_date<%s;" + cursor.execute(sql, (time_before,)) + cursor.close() + + +def check_launchpads(): + with connect().cursor() as cursor: + launchpads_sql = "SELECT * FROM launchpads" + cursor.execute(launchpads_sql) + launchpads = cursor.fetchall() + for launchpad in launchpads: + launchpad_info = get_launchpad_info(launchpad['id']) + if launchpad_info[1] is True: + sql = "DELETE FROM launchpads WHERE id=%s;" + cursor.execute(sql, (launchpad['id'],)) + elif launchpad_info[0] != launchpad['id']: + sql = "UPDATE launchpads SET title = %s WHERE id = %s;" + cursor.execute(sql, (launchpad_info[0], launchpad['id'],)) + cursor.close() + + +def free_storage(): + with connect().cursor() as cursor: + files_sql = "SELECT id, name, user_id, launchpad_id FROM files ORDER BY modified_date;" + cursor.execute(files_sql) + files = cursor.fetchall() + for file in files: + os.remove(os.path.join(app.config['BASE_DIR'], 'files', str(file['user_id']), str(file['launchpad_id']), + file['name'])) + logging.info('Outdated file deleted: {}/{}/{}'.format(file['user_id'], file['launchpad_id'], file['name'])) + sql = "DELETE FROM files WHERE id=%s;" + cursor.execute(sql, (file['id'],)) + if get_usage_info()[0] < THRESHOLD: + break + cursor.close() + + +def send_weekly_reports(): + usage_info = get_usage_info() + subject = 'Weekly Report' + rounded_usage = round(usage_info[0], 4) + free_space = round(usage_info[1]/1000000, 2) + with connect().cursor() as cursor: + sql = 'SELECT n.name, i.upload_count, i.total_size FROM openid_users n RIGHT JOIN ' \ + '(SELECT user_id, COUNT(*) AS upload_count, ROUND(SUM(file_size)/1000000, 2) AS total_size FROM files ' \ + 'WHERE modified_date > NOW() - INTERVAL 1 WEEK GROUP BY user_id)i ' \ + 'ON n.id = i.user_id;' + cursor.execute(sql) + reports_by_user = cursor.fetchall() + reports_by_user_table = '' \ + '' \ + '' \ + '' \ + '' + for report in reports_by_user: + reports_by_user_table = reports_by_user_table + '' \ + '' \ + '' % (report["name"], report["upload_count"], report["total_size"]) + reports_by_user_table = reports_by_user_table + '
NameNumber of UploadsTotal Upload Size
%s%s%s
' + with app.app_context(): + for admin in SERVER_ADMINS: + html_body = '

Hello %s,' \ + '
Here is the upload report for last week:
%s' \ + '
There are %s space used and %s MB of space left.' \ + % (admin["name"], reports_by_user_table, rounded_usage, usage_info[0]) + + msg = Message(subject, + html=html_body, + recipients=[admin['email']]) + mail.send(msg) + + +# @app.route('/storage_full/') +def if_storage_full(): + usage_info = get_usage_info() + if usage_info[0] > THRESHOLD: + subject = 'Storage is Nearly Full' + rounded_usage = round(usage_info[0], 4) + with app.app_context(): + for admin in SERVER_ADMINS: + html_body = '

Hello %s,' \ + '
The logfiles uploaded took %s of the server\'s total disk space.' \ + 'Oldest files will be deleted to free space.

' % (admin["name"], rounded_usage) + + msg = Message(subject, + html=html_body, + recipients=[admin['email']]) + mail.send(msg) + free_storage() + + # with smtplib.SMTP('mail.wrs.com', 587) as smtp: + # # smtp.ehlo() + # smtp.starttls() + # smtp.ehlo() + # + # # smtp.login(EMAIL_ADDRESS, EMAIL_PASSWORD) + # smtp.login('Nathan.Chen@windriver.com', 'MyPassword') + # + # subject = 'Storage is full' + # body = 'Do something' + # + # msg = f'Subject: {subject}\n\n{body}' + # + # smtp.sendmail('Nathan.Chen@windriver.com', 'wrcollecttesting1@gmail.com', msg) + + +scheduler = BackgroundScheduler() +scheduler.add_job(func=delete_old_files, trigger="cron", day='1') +scheduler.add_job(func=check_launchpads, trigger="cron", day_of_week='mon-fri') +scheduler.add_job(func=if_storage_full, trigger="cron", minute='00') +scheduler.add_job(func=send_weekly_reports, trigger="cron", day_of_week='mon') +scheduler.start() + +atexit.register(lambda: scheduler.shutdown()) + + +# @app.route('/usage/') +def get_usage_info(): + # return str(shutil.disk_usage(app.config['BASE_DIR'])) + # return str(shutil.disk_usage(os.path.join(app.config['BASE_DIR'], 'files')).used) + # return str(shutil.disk_usage(os.path.join(app.config['BASE_DIR'], 'files'))) + disk_usage_info = shutil.disk_usage(os.path.join(app.config['BASE_DIR'], 'files')) + usage_perc = disk_usage_info.used/disk_usage_info.total + return [usage_perc, disk_usage_info.free] + # return str(usage_perc) + + +def is_allowed(filename): + return '.' in filename and (filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS + or filename.rsplit('.', 2)[1] == 'tar') \ + and filename.rsplit('.', 1)[1] not in DISALLOWED_EXTENSIONS + + +def get_size(fobj): + if fobj.content_length: + return fobj.content_length + + try: + pos = fobj.tell() + fobj.seek(0, 2) # seek to end + size = fobj.tell() + fobj.seek(pos) # back to original position + return size + except (AttributeError, IOError): + pass + + # in-memory file object that doesn't support seeking or tell + return 0 # assume small enough + + +def connect(): + return pymysql.connect(host='db', + user='root', + password='Wind2019', + db='collect', + charset='utf8mb4', + cursorclass=pymysql.cursors.DictCursor, + autocommit=True) + + +def confirmation_required(desc_fn): + def inner(f): + @wraps(f) + def wrapper(*args, **kwargs): + if request.args.get('confirm') != '1': + desc = desc_fn() + return redirect(url_for('confirm', desc=desc, action_url=quote(request.url))) + return f(*args, **kwargs) + + return wrapper + + return inner + + +def get_launchpad_info(lp): + lp_id = lp + if isinstance(lp, dict): + lp_id = int(lp['launchpad_id']) + cachedir = os.path.join(app.config['BASE_DIR'], '.launchpadlib/cache/') + launchpad = Launchpad.login_anonymously('just testing', 'production', cachedir, version='devel') + try: + bug_one = launchpad.bugs[lp_id] + # return str(type(bug_one)) + # return bug_one.bug_tasks.entries[0]['title'] + is_starlingx = any(entry['bug_target_name'] == 'starlingx' for entry in bug_one.bug_tasks.entries) + if not is_starlingx: + return 'You need to choose a valid StarlingX Launchpad' + closed = all(entry['date_closed'] is not None for entry in bug_one.bug_tasks.entries) + return [bug_one.title, closed] + except KeyError: + return 'Launchpad bug id does not exist' + + +@app.errorhandler(413) +@app.errorhandler(RequestEntityTooLarge) +def error413(e): + # flash(u'Error: File size exceeds the 16GB limit') + # return render_template('index.html'), 413 + # return render_template('413.html'), 413 + return 'File Too Large', 413 + + +@app.errorhandler(401) +def error401(e): + flash(u'Error: You are not logged in') + return redirect(url_for('login', next=oid.get_next_url())) + + +@app.route('/confirm') +def confirm(): + desc = request.args['desc'] + action_url = unquote(request.args['action_url']) + + return render_template('_confirm.html', desc=desc, action_url=action_url) + + +def confirm_delete_profile(): + return "Are you sure you want to delete your profile? All your files will be removed." + + +def confirm_delete_file(): + return "Are you sure you want to delete this file?" + + +@app.before_request +def before_request(): + g.user = None + g.connection = connect() + if 'openid' in session: + with g.connection.cursor() as cursor: + sql = "select * from openid_users where openid = %s;" + cursor.execute(sql, (session['openid'],)) + g.user = cursor.fetchone() + cursor.close() + + +@app.after_request +def after_request(response): + return response + + +@app.route('/') +def index(): + return render_template('index.html') + + +@app.route('/login/', methods=['GET', 'POST']) +@oid.loginhandler +def login(): + if g.user is not None: + return redirect(oid.get_next_url()) + if request.method == 'POST': + openid = "https://launchpad.net/~" + request.form.get('openid') + if openid: + pape_req = pape.Request([]) + return oid.try_login(openid, ask_for=['email', 'nickname'], + ask_for_optional=['fullname'], + extensions=[pape_req]) + return render_template('login.html', next=oid.get_next_url(), + error=oid.fetch_error()) + + +@oid.after_login +def create_or_login(resp): + """This is called when login with OpenID succeeded and it's not + necessary to figure out if this is the users's first login or not. + This function has to redirect otherwise the user will be presented + with a terrible URL which we certainly don't want. + """ + session['openid'] = resp.identity_url + if 'pape' in resp.extensions: + pape_resp = resp.extensions['pape'] + session['auth_time'] = pape_resp.auth_time + with g.connection.cursor() as cursor: + sql = "select * from openid_users where openid = %s;" + cursor.execute(sql, (resp.identity_url,)) + user = cursor.fetchone() + cursor.close() + # user = User.query.filter_by(openid=resp.identity_url).first() + if user is not None: + flash(u'Successfully signed in') + g.user = user + return redirect(oid.get_next_url()) + return redirect(url_for('create_profile', next=oid.get_next_url(), + name=resp.fullname or resp.nickname, + email=resp.email)) + + +@app.route('/create-profile/', methods=['GET', 'POST']) +def create_profile(): + """If this is the user's first login, the create_or_login function + will redirect here so that the user can set up his profile. + """ + if g.user is not None or 'openid' not in session: + return redirect(url_for('index')) + if request.method == 'POST': + name = request.form['name'] + email = request.form['email'] + if not name: + flash(u'Error: you have to provide a name') + elif '@' not in email: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + with g.connection.cursor() as cursor: + sql = "INSERT INTO openid_users (name, email, openid) VALUES (%s, %s, %s);" + cursor.execute(sql, (name, email, session['openid'],)) + sql = "SELECT id FROM openid_users WHERE openid = %s;" + cursor.execute(sql, (session['openid'],)) + user_id = cursor.fetchone()['id'] + _dir = os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(user_id)) + os.mkdir(_dir) + cursor.close() + return redirect(oid.get_next_url()) + return render_template('create_profile.html', next_url=oid.get_next_url()) + + +@app.route('/profile/', methods=['GET', 'POST']) +def edit_profile(): + """Updates a profile""" + if g.user is None: + abort(401) + # form = dict(name=g.user.name, email=g.user.email) + # g_user_name = g.user.name + # g_user_email = g.user.email + form = {'name': g.user['name'], 'email': g.user['email']} + # form = {'name': 1, 'email': 1} + # form['name'] = g.user.name + # form['email'] = g.user.email + if request.method == 'POST': + if 'delete' in request.form: + with g.connection.cursor() as cursor: + sql = "DELETE FROM openid_users WHERE openid=%s;" + cursor.execute(sql, (session['openid'],)) + cursor.close() + shutil.rmtree(os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(g.user['id']))) + return redirect(oid.get_next_url()) + session['openid'] = None + flash(u'Profile deleted') + return redirect(url_for('index')) + form['name'] = request.form['name'] + form['email'] = request.form['email'] + if not form['name']: + flash(u'Error: you have to provide a name') + elif '@' not in form['email']: + flash(u'Error: you have to enter a valid email address') + else: + flash(u'Profile successfully created') + g.user['name'] = form['name'] + g.user['email'] = form['email'] + with g.connection.cursor() as cursor: + sql = "UPDATE openid_users SET name = %s, email = %s WHERE id = %s;" + cursor.execute(sql, (g.user['name'], g.user['email'], g.user['id'],)) + cursor.close() + return redirect(oid.get_next_url()) + # db_session.commit() + # return redirect(url_for('edit_profile')) + return render_template('edit_profile.html', form=form) + + +@app.route('/logout/') +def logout(): + session.pop('openid', None) + flash(u'You have been signed out') + return redirect(oid.get_next_url()) + + +@app.route('/check_launchpad/', methods=['GET', 'POST']) +def check_launchpad(launchpad_id): + with g.connection.cursor() as cursor: + sql = "SELECT * FROM launchpads WHERE id = %s;" + cursor.execute(sql, (launchpad_id,)) + launchpad_info = cursor.fetchone() + if launchpad_info: + launchpad_title = launchpad_info["title"] + else: + try: + launchpad_info = get_launchpad_info(int(launchpad_id)) + except ValueError: + res = make_response("Error: Launchpad bug id does not exist", 400) + return res + if launchpad_info == 'Launchpad bug id does not exist': + res = make_response("Error: Launchpad bug id does not exist", 400) + return res + elif launchpad_info == 'You need to choose a valid StarlingX Launchpad': + res = make_response("Error: You need to choose a valid StarlingX Launchpad", 400) + return res + elif launchpad_info[1] is True: + res = make_response("Error: Launchpad bug is closed", 400) + return res + sql = "INSERT INTO launchpads (id, title) VALUES (%s, %s);" + cursor.execute(sql, (launchpad_id, launchpad_info[0],)) + launchpad_title = launchpad_info[0] + res = make_response(launchpad_title, 200) + return res + + +@app.route('/upload/', methods=['GET', 'POST']) +def upload(): + try: + if g.user is None: + abort(401) + if request.method == 'POST': + launchpad_id = request.args.get('launchpad_id') + if launchpad_id == '': + res = make_response(jsonify({"message": "Error: you did not supply a valid Launchpad ID"}), 400) + return res + launchpad_info = 0 + + # with g.connection.cursor() as cursor: + # sql = "SELECT id FROM launchpads WHERE id = %s;" + # cursor.execute(sql, (launchpad_id,)) + # if not cursor.fetchone(): + # try: + # launchpad_info = get_launchpad_info(int(launchpad_id)) + # except ValueError: + # res = make_response(jsonify({"message": "Error: Launchpad bug id does not exist"}), 400) + # return res + # if launchpad_info == 'Launchpad bug id does not exist' or launchpad_info[1] is True: + # res = make_response(jsonify({"message": "Error: Launchpad bug id does not exist"}), 400) + # return res + # sql = "INSERT INTO launchpads (id, title) VALUES (%s, %s);" + # cursor.execute(sql, (launchpad_id, launchpad_info[0],)) + + # file_list = request.files.getlist('file') + # file_list = request.files['file'] + # file_list = request.files.get('file[]') + # + # res = make_response(jsonify({"message": str(file_list)}), 200) + # return res + + # file_size = get_size(f) + # if file_size > MAX_CONTENT_LENGTH: + # flash(u'Error: File size exceeds the 10GB limit') + # return redirect(oid.get_next_url()) + # for f in file_list: + f = request.files['file'] + if f and is_allowed(f.filename): + _launchpad_dir = os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(g.user['id']), launchpad_id) + conflict = request.args.get('conflict') + final_filename = secure_filename(f.filename) + if conflict == '1': + if final_filename.rsplit('.', 2)[1] == 'tar' and len(final_filename.rsplit('.', 2)) == 3: + tail = 1 + while True: + new_filename = final_filename.rsplit('.', 2)[0] + "_" + str(tail) + '.' \ + + final_filename.rsplit('.', 2)[1] + '.' + final_filename.rsplit('.', 2)[2] + with g.connection.cursor() as cursor: + file_name_sql = "SELECT id FROM files WHERE launchpad_id=%s AND name=%s AND user_id=%s;" + cursor.execute(file_name_sql, (launchpad_id, new_filename, g.user['id'])) + if cursor.fetchone(): + tail = tail + 1 + else: + break + else: + tail = 1 + while True: + new_filename = final_filename.rsplit('.', 1)[0] + "_" + str(tail) + '.' \ + + final_filename.rsplit('.', 1)[1] + with g.connection.cursor() as cursor: + file_name_sql = "SELECT id FROM files WHERE launchpad_id=%s AND name=%s AND user_id=%s;" + cursor.execute(file_name_sql, (launchpad_id, new_filename, g.user['id'])) + if cursor.fetchone(): + tail = tail + 1 + else: + break + final_filename = new_filename + if not os.path.isdir(_launchpad_dir): + os.mkdir(_launchpad_dir) + _full_dir = os.path.join(_launchpad_dir, final_filename) + f.save(_full_dir) + file_size = os.stat(_full_dir).st_size + # mimetype = f.mimetype + # flash(mimetype) + mime = magic.Magic(mime=True) + mimetype = mime.from_file(_full_dir) + if mimetype.startswith('image/', 0) or mimetype.startswith('video/', 0)\ + or mimetype in DISALLOWED_MIME_TYPES: + os.remove(_full_dir) + res = make_response(jsonify({ + "message": "Error: you did not supply a valid file in your request"}), 400) + return res + if mimetype in ['application/x-gzip', 'application/x-tar']: + tar = tarfile.open(_full_dir) + if not any((any(x in y for x in TARGETED_TAR_CONTENTS) for y in tar.getnames())): + res = make_response(jsonify({ + "message": "Error: you did not supply a valid collect file in your request"}), 400) + logging.info(tar.getnames()) + return res + if conflict == '0': + with g.connection.cursor() as cursor: + sql = "UPDATE files SET modified_date = %s, file_size = %s " \ + "WHERE user_id = %s AND name = %s AND launchpad_id = %s;" + cursor.execute( + sql, (datetime.now(), file_size, g.user['id'], final_filename, launchpad_id)) + cursor.close() + logging.info('User#{} re-uploaded file {} under launchpad bug#{}'. + format(g.user['id'], final_filename, launchpad_id)) + else: + with g.connection.cursor() as cursor: + sql = "INSERT INTO files (name, user_id, launchpad_id, modified_date, file_size)" \ + "VALUES (%s, %s, %s, %s, %s);" + cursor.execute(sql, (final_filename, g.user['id'], launchpad_id, datetime.now(), + file_size,)) + cursor.close() + logging.info('User#{} uploaded file {} under launchpad bug#{}'. + format(g.user['id'], final_filename, launchpad_id)) + else: + logging.error('User#{} tried to upload a file with invalid format'.format(g.user['id'])) + res = make_response(jsonify({"message": "Error: you did not supply a valid file in your request"}), 400) + return res + # except RequestEntityTooLarge: + # flash(u'Error: File size exceeds the 16GB limit') + # return redirect(oid.get_next_url()) + res = make_response(jsonify({"message": "file uploaded successfully: {}".format(f.filename)}), 200) + return res + return render_template('upload.html') + except RequestEntityTooLarge: + flash(u'Error: File size exceeds the 10GB limit') + return redirect(oid.get_next_url()) + + +@app.route('/user_files/', methods=['GET', 'POST']) +def list_user_files(): + """Updates a profile""" + if g.user is None: + abort(401) + user_files = [] + if request.method == 'GET': + with g.connection.cursor() as cursor: + search = request.args.get('search') + if search: + sql = "SELECT f.*, l.title FROM files f JOIN launchpads l ON f.launchpad_id = l.id " \ + "WHERE (launchpad_id = '{}' OR title LIKE '%{}%') AND user_id = {} " \ + "ORDER BY launchpad_id DESC, f.name;".format(search, search, g.user['id']) + # sql = "SELECT * FROM launchpads WHERE id IN (SELECT DISTINCT launchpad_id FROM files WHERE user_id = %s);" + cursor.execute(sql,) + else: + sql = "SELECT f.*, l.title FROM files f JOIN launchpads l ON f.launchpad_id = l.id " \ + "WHERE user_id = %s ORDER BY launchpad_id DESC;" + cursor.execute(sql, (g.user['id'],)) + user_files = cursor.fetchall() + cursor.close() + return render_template('user_files.html', user_files=user_files) + + +@app.route('/public_files/', methods=['GET', 'POST']) +def list_public_files(): + """Updates a profile""" + if g.user is None: + abort(401) + files = [] + if request.method == 'GET': + with g.connection.cursor() as cursor: + sql = "SELECT f.*, l.title, f.user_id, u.name AS user_name FROM files f JOIN launchpads l " \ + "ON f.launchpad_id = l.id JOIN openid_users u ON f.user_id = u.id;" + cursor.execute(sql) + files = cursor.fetchall() + cursor.close() + # if files: + # files = list(map(lambda f: f.update({'editable': (f['user_id'] == g.user)}))) + return render_template('public_files.html', public_files=files) + + +@app.route('/launchpads/', methods=['GET', 'POST']) +def list_all_launchpads(): + """Updates a profile""" + if g.user is None: + abort(401) + if request.method == 'GET': + with g.connection.cursor() as cursor: + search = request.args.get('search') + if search: + sql = "SELECT * FROM launchpads WHERE (id = '{}' OR title LIKE '%{}%') " \ + "AND id IN (SELECT DISTINCT launchpad_id FROM files) ORDER BY id DESC;".format(search, search) + # sql = "SELECT * FROM launchpads WHERE id IN (SELECT DISTINCT launchpad_id FROM files WHERE user_id = %s);" + cursor.execute(sql,) + else: + sql = "SELECT * FROM launchpads WHERE id IN (SELECT DISTINCT launchpad_id FROM files) ORDER BY id DESC;" + cursor.execute(sql,) + user_launchpads = cursor.fetchall() + cursor.close() + return render_template('launchpads.html', user_launchpads=user_launchpads) + + +@app.route('/launchpad/', methods=['GET', 'POST']) +def list_files_under_a_launchpad(launchpad_id): + """Updates a profile""" + if g.user is None: + abort(401) + if request.method == 'GET': + with g.connection.cursor() as cursor: + sql = "SELECT f.*, u.name AS user_name FROM files f " \ + "JOIN openid_users u ON f.user_id = u.id WHERE launchpad_id = %s;" + cursor.execute(sql, (launchpad_id,)) + launchpad_files = cursor.fetchall() + sql = "SELECT * FROM launchpads WHERE id = %s;" + cursor.execute(sql, (launchpad_id,)) + launchpad_info = cursor.fetchone() + cursor.close() + return render_template('launchpad.html', launchpad_files=launchpad_files, launchpad_info=launchpad_info) + # return render_template('user_files.html', user_launchpads=user_launchpads) + + +@app.route('/edit_file/', methods=['GET', 'POST']) +def edit_file(file_id): + """Updates a profile""" + if g.user is None: + abort(401) + with g.connection.cursor() as cursor: + sql = "SELECT * FROM files WHERE id = %s;" + cursor.execute(sql, (file_id,)) + user_files = cursor.fetchone() + form = {'name': user_files['name'], 'launchpad_id': user_files['launchpad_id']} + old_form = form.copy() + cursor.close() + if request.method == 'POST': + form['name'] = request.form['name'] + form['launchpad_id'] = request.form['launchpad_id'] + if not form['name']: + flash(u'Error: you have to provide a name') + else: + # _dir = os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(g.user['id'])) + # flash(os.path.join(_dir, old_form['launchpad_id'], old_form['name'])) + if old_form['name'] != form['name'] or old_form['launchpad_id'] != int(form['launchpad_id']): + _dir = os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(g.user['id'])) + _new_dir = os.path.join(_dir, str(form['launchpad_id'])) + if old_form['launchpad_id'] != form['launchpad_id']: + with g.connection.cursor() as cursor: + sql = "SELECT * FROM launchpads WHERE id = %s;" + cursor.execute(sql, (form['launchpad_id'],)) + launchpad_info = cursor.fetchone() + if not launchpad_info: + try: + launchpad_info = get_launchpad_info(int(form['launchpad_id'])) + except ValueError: + res = make_response("Error: Launchpad bug id does not exist", 400) + return res + if launchpad_info == 'Launchpad bug id does not exist': + flash(u'Error: Launchpad bug id does not exist') + return redirect(oid.get_next_url()) + elif launchpad_info == 'You need to choose a valid StarlingX Launchpad': + flash(u'Error: You need to choose a valid StarlingX Launchpad') + return redirect(oid.get_next_url()) + elif launchpad_info[1] is True: + flash(u'Error: Launchpad bug is closed') + return redirect(oid.get_next_url()) + else: + sql = "INSERT INTO launchpads (id, title) VALUES (%s, %s);" + cursor.execute(sql, (form['launchpad_id'], launchpad_info[0],)) + cursor.close() + if not os.path.isdir(_new_dir): + os.mkdir(_new_dir) + os.rename(os.path.join(_dir, str(old_form['launchpad_id']), old_form['name']), + os.path.join(_dir, str(form['launchpad_id']), form['name'])) + if old_form['name'] == form['name']: + logging.info('User#{} changed file {}/{} to {}/{}'. + format(g.user['id'], old_form['launchpad_id'], + old_form['name'], form['launchpad_id'], form['name'])) + with g.connection.cursor() as cursor: + sql = "UPDATE files SET name = %s, launchpad_id = %s, modified_date = %s WHERE id = %s;" + cursor.execute(sql, (form['name'], form['launchpad_id'], datetime.now(), file_id,)) + cursor.close() + flash(u'File information successfully updated') + return redirect(url_for('edit_file', file_id=file_id, form=form)) + return render_template('edit_file.html', file_id=file_id, form=form) + + +@app.route('/delete_file', methods=['GET', 'POST']) +# @confirmation_required(confirm_delete_file) +def delete_file(): + """Updates a profile""" + if g.user is None: + abort(401) + file_id = request.form['id'] + with g.connection.cursor() as cursor: + file_name_sql = "SELECT name, launchpad_id FROM files WHERE id=%s;" + cursor.execute(file_name_sql, (file_id,)) + file_info = cursor.fetchone() + os.remove(os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(g.user['id']), str(file_info['launchpad_id']), + file_info['name'])) + + sql = "DELETE FROM files WHERE id=%s;" + cursor.execute(sql, (file_id,)) + cursor.close() + logging.info('User#{} deleted file {} under launchpad bug#{}'. + format(g.user['id'], secure_filename(file_info['name']), file_info['launchpad_id'])) + flash(u'File deleted') + return redirect(url_for('list_user_files')) + + +@app.route('/download_file/', methods=['GET', 'POST']) +def download_file(file_id): + with g.connection.cursor() as cursor: + file_name_sql = "SELECT name, launchpad_id, user_id FROM files WHERE id=%s;" + cursor.execute(file_name_sql, (file_id,)) + file_info = cursor.fetchone() + download_link = os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(file_info['user_id']), + str(file_info['launchpad_id']), file_info['name']) + cursor.close() + return send_file(download_link, attachment_filename=file_info['name'], as_attachment=True, cache_timeout=0) + + +@app.route('/download_launchpad/', methods=['GET', 'POST']) +def download_launchpad(launchpad_id): + zipf = zipfile.ZipFile('{}.zip'.format(launchpad_id), 'w', zipfile.ZIP_DEFLATED) + with g.connection.cursor() as cursor: + launchpad_file_sql = "SELECT f.name, f.user_id, f.launchpad_id, u.name AS uploader FROM files f " \ + "JOIN openid_users u ON f.user_id = u.id WHERE launchpad_id=%s;" + cursor.execute(launchpad_file_sql, (launchpad_id,)) + files = cursor.fetchall() + for file in files: + zipf.write(os.path.join(app.config['BASE_DIR'], 'files/{}/'.format(file['user_id']), + str(file['launchpad_id']), file['name']), + os.path.join(file['uploader'], file['name'])) + cursor.close() + zipf.close() + + @after_this_request + def remove_file(response): + try: + file_path = '{}.zip'.format(launchpad_id) + os.remove(file_path) + file_handle = open(file_path, 'r') + file_handle.close() + except Exception as error: + app.logger.error("Error removing or closing downloaded file handle", error) + return response + return send_file('{}.zip'.format(launchpad_id), mimetype='zip', + attachment_filename='{}.zip'.format(launchpad_id), as_attachment=True, cache_timeout=0) + + +@app.route('/file_exists/', methods=['GET', 'POST']) +def file_exists(): + file_name = request.args.get('file_name') + launchpad_id = request.args.get('launchpad_id') + if not is_allowed(file_name): + return '-1' + with g.connection.cursor() as cursor: + file_name_sql = "SELECT id FROM files WHERE launchpad_id=%s AND name=%s AND user_id=%s;" + cursor.execute(file_name_sql, (launchpad_id, secure_filename(file_name), g.user['id'])) + if cursor.fetchone(): + return '1' + else: + return '0' + + +@app.route('/view_log/', methods=['GET', 'POST']) +def view_log(): + return send_file('collect.log', mimetype='text/plain') + + +if __name__ == "__main__": + app.run(debug=True, host='0.0.0.0') diff --git a/tools/collect_filesystem/app/requirements.txt b/tools/collect_filesystem/app/requirements.txt new file mode 100644 index 00000000..888244dc --- /dev/null +++ b/tools/collect_filesystem/app/requirements.txt @@ -0,0 +1,2 @@ +Flask==1.1.1 +mysql-connector \ No newline at end of file diff --git a/tools/collect_filesystem/app/static/main.js b/tools/collect_filesystem/app/static/main.js new file mode 100644 index 00000000..48f09cc6 --- /dev/null +++ b/tools/collect_filesystem/app/static/main.js @@ -0,0 +1,415 @@ +function ConfirmDelete(elem) { + localStorage.setItem('deleteId', $(elem).attr('data-id')); + $('#deleteModal').modal(); +} + +function Delete() { + $.ajax({ + url: '/delete_file', + data: { + id: localStorage.getItem('deleteId') + }, + type: 'POST', + success: function(res) { + $('#deleteModal').modal('hide'); + location.reload(); + }, + error: function(error) { + console.log(error); + } + }); +} + +// Get a reference to the progress bar, wrapper & status label +var progress_wrapper = document.getElementById("progress_wrapper"); + +// Get a reference to the 3 buttons +var upload_btn = document.getElementById("upload_btn"); +var loading_btn = document.getElementById("loading_btn"); +var cancel_btn = document.getElementById("cancel_btn"); + +// Get a reference to the alert wrapper +var alert_wrapper = document.getElementById("alert_wrapper"); + +// Get a reference to the file input element & input label +var input = document.getElementById("file_input"); +var launchpad_input = document.getElementById("launchpad_input"); +var file_input_label = document.getElementById("file_input_label"); + +var search_input_label = document.getElementById("search_input_label"); +var launchpad_info = document.getElementById("launchpad_info"); + +var upload_count = 0; + +function search() { + search_input = search_input_label.value; + location.search = "?search="+search_input; +} + +// Function to show alerts +function show_alert(message, alert) { + + alert_wrapper.innerHTML = alert_wrapper.innerHTML + ` + + ` + +} + +function revert() { + var tbody = $('table tbody'); + tbody.html($('tr',tbody).get().reverse()); + if (document.getElementById("table_order").classList.contains("fa-caret-square-o-down")) { + document.getElementById("table_order").classList.remove("fa-caret-square-o-down"); + document.getElementById("table_order").classList.add("fa-caret-square-o-up"); + } else { + document.getElementById("table_order").classList.remove("fa-caret-square-o-up"); + document.getElementById("table_order").classList.add("fa-caret-square-o-down"); + } +} + +function upload() { + + upload_count = 0 + + // Reject if the file input is empty & throw alert + if (!input.value) { + + show_alert("No file selected", "warning") + + return; + + } + + var request = new XMLHttpRequest(); + var launchpad_id = launchpad_input.value; + + request.open("get", "http://128.224.141.2:5000/check_launchpad/"+launchpad_id); + request.send(); + + // Clear any existing alerts + alert_wrapper.innerHTML = ""; + + request.addEventListener("load", function (e) { + + if (request.status == 200) { + + // Hide the upload button + upload_btn.classList.add("d-none"); + + // Show the loading button + loading_btn.classList.remove("d-none"); + + // Show the cancel button + cancel_btn.classList.remove("d-none"); + + // Show the progress bar + progress_wrapper.classList.remove("d-none"); + + // Show the progress bar + launchpad_info.classList.remove("d-none"); + + launchpad_info.innerHTML = 'Launchpad title: '+request.response; + + // Disable the input during upload + input.disabled = true; + launchpad_input.disabled = true; + + progress_wrapper.innerHTML = "" + + for (var i = 0; i < input.files.length; i++) { + progress_wrapper.innerHTML = progress_wrapper.innerHTML + ` +
+ + + + + +
+
+
+
` + } + + for (var i = 0; i < input.files.length; i++) { + upload_single_file(input.files[i], i); + } + + } + else { + + // Reset the input placeholder + file_input_label.innerText = "Select file or drop it here to upload"; + launchpad_input.innerText = ""; + + show_alert(`${request.response}`, "danger"); + + } + + }); + +// reset(); +} + +// Function to upload single file +function upload_single_file(file, i) { + + var progress_wrapper_single = document.getElementById(`progress_wrapper_${i}`); + var progress = document.getElementById(`progress_${i}`); + var progress_status = document.getElementById(`progress_status_${i}`); + + var cancel_btn_single = document.getElementById(`cancel_btn_${i}`); + var ignore_btn_single = document.getElementById(`ignore_btn_${i}`); + var overwrite_btn_single = document.getElementById(`overwrite_btn_${i}`); + var rename_btn_single = document.getElementById(`rename_btn_${i}`); + + var url = "http://128.224.141.2:5000/upload/" + + // Create a new FormData instance + var data = new FormData(); + + // Create a XMLHTTPRequest instance + var request = new XMLHttpRequest(); + var request_file_check = new XMLHttpRequest(); + + // Set the response type + request.responseType = "json"; + + // Get a reference to the files +// var file = input.files[0]; + // Get a reference to the launchpad id + var launchpad_id = launchpad_input.value; + +// // Get a reference to the filename +// var filename = file.name; + + // Get a reference to the filesize & set a cookie +// var filesize = file.size; +// document.cookie = `filesize=${filesize}`; + + // Append the file to the FormData instance +// data.append("file", file); + + request_file_check.open("get", '/file_exists/?launchpad_id='+launchpad_id+'&file_name='+file.name); + request_file_check.send(); + + request_file_check.addEventListener("load", function (e) { + if (request_file_check.responseText == '0'){ + // Open and send the request + request.open("post", url+"?launchpad_id="+launchpad_id); + data = new FormData(); + data.append("file", file); + request.send(data); + } else if (request_file_check.responseText == '1'){ + progress_status.innerText = `File already exists: ${file.name}`; + progress_status.style.color = 'red'; + cancel_btn_single.classList.add("d-none"); + ignore_btn_single.classList.remove("d-none"); + overwrite_btn_single.classList.remove("d-none"); + rename_btn_single.classList.remove("d-none"); + } else { + show_alert('Error: you did not supply a valid file in your request', "warning"); + upload_count++; + + if (upload_count == input.files.length){ + reset(); + } + } + }); + + ignore_btn_single.addEventListener("click", function () { + + progress_status.style.color = 'black'; + + cancel_btn_single.classList.remove("d-none"); + ignore_btn_single.classList.add("d-none"); + overwrite_btn_single.classList.add("d-none"); + rename_btn_single.classList.add("d-none"); + + show_alert(`Upload cancelled: ${file.name}`, "primary"); + + progress_wrapper_single.classList.add("d-none"); + + upload_count++; + + if (upload_count == input.files.length){ + reset(); + } + + }) + + overwrite_btn_single.addEventListener("click", function () { + + progress_status.style.color = 'black'; + + cancel_btn_single.classList.remove("d-none"); + ignore_btn_single.classList.add("d-none"); + overwrite_btn_single.classList.add("d-none"); + rename_btn_single.classList.add("d-none"); + + request.open("post", url+"?launchpad_id="+launchpad_id+'&conflict=0'); + data = new FormData(); + data.append("file", file); + request.send(data); + + }) + + rename_btn_single.addEventListener("click", function () { + + progress_status.style.color = 'black'; + + cancel_btn_single.classList.remove("d-none"); + ignore_btn_single.classList.add("d-none"); + overwrite_btn_single.classList.add("d-none"); + rename_btn_single.classList.add("d-none"); + + request.open("post", url+"?launchpad_id="+launchpad_id+'&conflict=1'); + data = new FormData(); + data.append("file", file); + request.send(data); + + }) + + // request progress handler + request.upload.addEventListener("progress", function (e) { + + // Get the loaded amount and total filesize (bytes) + var loaded = e.loaded; + var total = e.total; + + // Calculate percent uploaded + var percent_complete = (loaded / total) * 100; + + // Update the progress text and progress bar + progress.setAttribute("style", `width: ${Math.floor(percent_complete)}%`); + progress_status.innerText = `${Math.floor(percent_complete)}% uploaded: ${file.name}`; + + if (loaded == total) { + progress_status.innerText = `Saving file: ${file.name}`; + } + + }) + + // request load handler (transfer complete) + request.addEventListener("load", function (e) { + + if (request.status == 200) { + + show_alert(`${request.response.message}`, "success"); + + } + else { + + show_alert(`${request.response.message}`, "danger"); + + } + + progress_wrapper_single.classList.add("d-none"); + + upload_count++; + + if (upload_count == input.files.length){ + reset(); + } + + }); + + // request error handler + request.addEventListener("error", function (e) { + + show_alert(`Error uploading file: ${file.name}`, "warning"); + + progress_wrapper_single.classList.add("d-none"); + + upload_count++; + + if (upload_count == input.files.length){ + reset(); + } + + }); + + // request abort handler + request.addEventListener("abort", function (e) { + + show_alert(`Upload cancelled: ${file.name}`, "primary"); + + progress_wrapper_single.classList.add("d-none"); + + upload_count++; + + if (upload_count == input.files.length){ + reset(); + } + + }); + + cancel_btn.addEventListener("click", function () { + + request.abort(); + + }) + + cancel_btn_single.addEventListener("click", function () { + + request.abort(); + + }) + +} + +// Function to update the input placeholder +function input_filename() { +// file_input_label.innerText = typeof input.files; +// var all_files = input.files.values().reduce(function (accumulator, file) { +// return accumulator + file.name; +// }, 0); + + var all_files = input.files[0].name; + + for (var i = 1; i < input.files.length; i++){ + all_files = all_files + ', ' + input.files[i].name + } + file_input_label.innerText = all_files; + +// file_input_label.innerText = input.files[0].name; +// file_input_label.innerText = input.files.toString(); + +} + +// Function to reset the page +function reset() { + + // Clear the input + input.value = null; + + // Hide the cancel button + cancel_btn.classList.add("d-none"); + + // Reset the input element + input.disabled = false; + launchpad_input.disabled = false; + + // Show the upload button + upload_btn.classList.remove("d-none"); + + // Hide the loading button + loading_btn.classList.add("d-none"); + + // Hide the progress bar + progress_wrapper.classList.add("d-none"); + + // Reset the input placeholder + file_input_label.innerText = "Select file or drop it here to upload"; + launchpad_input.innerText = ""; + + // Show the progress bar + launchpad_info.classList.add("d-none"); + + launchpad_info.innerHTML = ""; + +} \ No newline at end of file diff --git a/tools/collect_filesystem/app/static/openid.png b/tools/collect_filesystem/app/static/openid.png new file mode 100644 index 0000000000000000000000000000000000000000..ce7954ab12080e1cfb0583669a58267bea008a44 GIT binary patch literal 433 zcmV;i0Z#sjP)DL1dJy@3D#0X|7Y zK~y-)wbMUH)^QL9@X!15!y(?mMM2P4p@xE{I+t9cA<2T^g}0^6DYpnD0w*VNtlcRZ zqPZNxAflj-7X_<9aESg4Qc-~~QK-vr5W&}8!-Io6?)yC4-E&`#+O{VRZBLrouU zd>!vFekcM=pw{U@^?ygK>`pSXz(rifnd6Jor+e3*)qd8`ZLVFpyNVVT@i-#>Waf!t z7r2RejNk{BiuSi&oGjYkKjQF~8cv)sk21Hd98k$YA6TgE+yQK5?aW&2SsvO9u^ zc!QfI$%D+C%gp|6IuH@}Z~@D>jZc~R^4H+#7}|L9Plr@W8n}eB_2+ZH`&J*_sVrDO bvfsJ`%6MX|2>A&`00000NkvXXu0mjf^=ZFi literal 0 HcmV?d00001 diff --git a/tools/collect_filesystem/app/static/style.css b/tools/collect_filesystem/app/static/style.css new file mode 100644 index 00000000..a52b96a6 --- /dev/null +++ b/tools/collect_filesystem/app/static/style.css @@ -0,0 +1,46 @@ +body { + font-family: 'Georgia', serif; + font-size: 16px; + margin: 30px; + padding: 0; +} + +a { + color: #335E79; +} + +p.message { + color: #335E79; + padding: 10px; + background: #CADEEB; +} + +p.error { + color: #783232; + padding: 10px; + background: #EBCACA; +} + +input { + font-family: 'Georgia', serif; + font-size: 16px; + border: 1px solid black; + color: #335E79; + padding: 2px; +} + +input[type="submit"] { + background: #CADEEB; + color: #335E79; + border-color: #335E79; +} + +#basic-addon3 { + background: url(openid.png) 4px no-repeat; + background-color: #e9ecef; + padding-left: 24px; +} + +h1, h2 { + font-weight: normal; +} diff --git a/tools/collect_filesystem/app/templates/413.html b/tools/collect_filesystem/app/templates/413.html new file mode 100644 index 00000000..9f3a2dc2 --- /dev/null +++ b/tools/collect_filesystem/app/templates/413.html @@ -0,0 +1,7 @@ +{% extends "layout.html" %} +{% block title %}Page Not Found{% endblock %} +{% block body %} +

Page Not Found

+

What you were looking for is just not there. +

go somewhere nice +{% endblock %} \ No newline at end of file diff --git a/tools/collect_filesystem/app/templates/_confirm.html b/tools/collect_filesystem/app/templates/_confirm.html new file mode 100644 index 00000000..b8caecb1 --- /dev/null +++ b/tools/collect_filesystem/app/templates/_confirm.html @@ -0,0 +1,7 @@ + +

{{ desc }}

+
+ + +
+ \ No newline at end of file diff --git a/tools/collect_filesystem/app/templates/bak/upload.html b/tools/collect_filesystem/app/templates/bak/upload.html new file mode 100644 index 00000000..e513f4b3 --- /dev/null +++ b/tools/collect_filesystem/app/templates/bak/upload.html @@ -0,0 +1,51 @@ +{% extends "layout.html" %} +{% block title %}Sign in{% endblock %} +{% block body %} +

Upload

+ +
+
+
+ + +
Launchpad ID:
+
+
+
+ + + + + + + + + + + + + + + + + +
+ + +{% endblock %} \ No newline at end of file diff --git a/tools/collect_filesystem/app/templates/create_profile.html b/tools/collect_filesystem/app/templates/create_profile.html new file mode 100644 index 00000000..e024a627 --- /dev/null +++ b/tools/collect_filesystem/app/templates/create_profile.html @@ -0,0 +1,28 @@ +{% extends "layout.html" %} +{% block title %}Create Profile{% endblock %} +{% block body %} +

Create Profile

+

+ Hey! This is the first time you signed in on this website. In + order to proceed we need some extra information from you: +

+
+
+ Name: +
+ +
+
+
+ E-Mail: +
+ +
+

+ + +

+

+ If you don't want to proceed, you can sign out again. +{% endblock %} diff --git a/tools/collect_filesystem/app/templates/edit_file.html b/tools/collect_filesystem/app/templates/edit_file.html new file mode 100644 index 00000000..f942632a --- /dev/null +++ b/tools/collect_filesystem/app/templates/edit_file.html @@ -0,0 +1,27 @@ +{% extends "layout.html" %} +{% block title %}Edit Profile{% endblock %} +{% block body %} +

Edit File

+
+ +
+
+ Name: +
+ +
+
+
+ Launchpad ID: +
+ +
+ + + + + +

+ +

+{% endblock %} diff --git a/tools/collect_filesystem/app/templates/edit_profile.html b/tools/collect_filesystem/app/templates/edit_profile.html new file mode 100644 index 00000000..ffaf1e17 --- /dev/null +++ b/tools/collect_filesystem/app/templates/edit_profile.html @@ -0,0 +1,22 @@ +{% extends "layout.html" %} +{% block title %}Edit Profile{% endblock %} +{% block body %} +

Edit Profile

+
+
+
+ Name: +
+ +
+
+
+ E-Mail: +
+ +
+

+ + +

+{% endblock %} diff --git a/tools/collect_filesystem/app/templates/index.html b/tools/collect_filesystem/app/templates/index.html new file mode 100644 index 00000000..54f5161b --- /dev/null +++ b/tools/collect_filesystem/app/templates/index.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} +{% block body %} +

Overview

+ {% if g.user %} +

+ Hello {{ g.user.name }}! + {% endif %} +

+ This is just an example page so that something is here. +{% endblock %} diff --git a/tools/collect_filesystem/app/templates/launchpad.html b/tools/collect_filesystem/app/templates/launchpad.html new file mode 100644 index 00000000..86e3baa3 --- /dev/null +++ b/tools/collect_filesystem/app/templates/launchpad.html @@ -0,0 +1,40 @@ +{% extends "layout.html" %} +{% block body %} +

+ {{ launchpad_info['title'] }} + + +

+ + + + + + + + {% for launchpad_file in launchpad_files %} + + + + + {% if g.user['id'] == launchpad_file['user_id'] %} + + {% else %} + + {% endif %} + + {% endfor %} +
NameUploaded byLast Modified
{{ launchpad_file['name'] }}{{ launchpad_file['user_name'] }} {{ launchpad_file['modified_date'] }} + + + + + +
+{% endblock %} diff --git a/tools/collect_filesystem/app/templates/launchpads.html b/tools/collect_filesystem/app/templates/launchpads.html new file mode 100644 index 00000000..d55e4cf1 --- /dev/null +++ b/tools/collect_filesystem/app/templates/launchpads.html @@ -0,0 +1,44 @@ +{% extends "layout.html" %} +{% block body %} +

Launchpads

+
+
+ +
+ +
+
+
+ {% if user_launchpads|length %} + + + + + + + + + {% for user_launchpad in user_launchpads %} + + + + + + + {% endfor %} +
+ LP Files + Launchpad LinkLaunchpad Title
+ {{ user_launchpad['id'] }} + + https://bugs.launchpad.net/bugs/{{ user_launchpad['id'] }} + {{ user_launchpad['title'] }}
+ {% else %} +
Your search returns no result
+ {% endif %} + + + +{% endblock %} diff --git a/tools/collect_filesystem/app/templates/layout.html b/tools/collect_filesystem/app/templates/layout.html new file mode 100644 index 00000000..9f6333cd --- /dev/null +++ b/tools/collect_filesystem/app/templates/layout.html @@ -0,0 +1,28 @@ + +{% block title %}Welcome{% endblock %} | Flask OpenID Example + + + + + + + +

Collect

+ +{% for message in get_flashed_messages() %} +

{{ message }} +{% endfor %} +{% block body %}{% endblock %} diff --git a/tools/collect_filesystem/app/templates/login.html b/tools/collect_filesystem/app/templates/login.html new file mode 100644 index 00000000..4eb494b9 --- /dev/null +++ b/tools/collect_filesystem/app/templates/login.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} +{% block title %}Sign in{% endblock %} +{% block body %} +

Sign in

+
+ {% if error %}

Error: {{ error }}

{% endif %} +

+ Enter your Ubuntu One OpenID: +

+
+ https://launchpad.net/~ +
+ +
+ + +
+{% endblock %} diff --git a/tools/collect_filesystem/app/templates/public_files.html b/tools/collect_filesystem/app/templates/public_files.html new file mode 100644 index 00000000..e4d27206 --- /dev/null +++ b/tools/collect_filesystem/app/templates/public_files.html @@ -0,0 +1,64 @@ +{% extends "layout.html" %} +{% block body %} + + + + + + + +

Files

+ {% if public_files|length %} + + + + + + + + + + + {% for public_file in public_files %} + + + + + + {% if g.user['id'] == public_file['user_id'] %} + + {% else %} + + {% endif %} + + {% endfor %} +
NameLaunchpadUploaded byLast Modified
{{ public_file['name'] }} + {{ public_file['title'] }} #{{ public_file['launchpad_id'] }} + {{ public_file['user_name'] }} {{ public_file['modified_date'] }} + + + + + +
+ {% else %} +
Opps! There is no file here
+ {% endif %} + + +{% endblock %} diff --git a/tools/collect_filesystem/app/templates/upload.html b/tools/collect_filesystem/app/templates/upload.html new file mode 100644 index 00000000..7b2cd8ce --- /dev/null +++ b/tools/collect_filesystem/app/templates/upload.html @@ -0,0 +1,58 @@ +{% extends "layout.html" %} +{% block title %}Sign in{% endblock %} +{% block body %} + + + +
+
+
+ +
+ +

Upload Log Files

+ +
+
+ + + + + + +
+
+ +
+
+ Launchpad ID: +
+ +
+ +

+ + + + + + + +
+ +
+ +
+ +
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/tools/collect_filesystem/app/templates/user_files.html b/tools/collect_filesystem/app/templates/user_files.html new file mode 100644 index 00000000..5f77c0d1 --- /dev/null +++ b/tools/collect_filesystem/app/templates/user_files.html @@ -0,0 +1,72 @@ +{% extends "layout.html" %} +{% block body %} + + + + + + + +

Files

+
+
+ +
+ +
+
+
+ {% if user_files|length %} + + + + + + + + + + + {% for user_file in user_files %} + + + + + + + + {% endfor %} +
NameLaunchpad + LP Files + Last Modified
{{ user_file['name'] }}{{ user_file['title'] }} + #{{ user_file['launchpad_id'] }} + {{ user_file['modified_date'] }} + + + + + +
+ {% else %} +
You have not upload anything yet
+ {% endif %} + + +{% endblock %} diff --git a/tools/collect_filesystem/db/init.sql b/tools/collect_filesystem/db/init.sql new file mode 100644 index 00000000..5a969ee7 --- /dev/null +++ b/tools/collect_filesystem/db/init.sql @@ -0,0 +1,7 @@ +CREATE DATABASE collect; +USE collect; +CREATE TABLE openid_users(id INT not null AUTO_INCREMENT, name VARCHAR(60), email VARCHAR(200), openid VARCHAR(200), PRIMARY KEY (id)); +CREATE TABLE launchpads(id INT not null, title VARCHAR(200), PRIMARY KEY (id)); +CREATE TABLE files(id INT not null AUTO_INCREMENT, name VARCHAR(60), user_id INT not null, launchpad_id INT not null, modified_date TIMESTAMP, PRIMARY KEY (id), file_size FLOAT NOT NULL, +FOREIGN KEY (user_id) REFERENCES openid_users(id) ON DELETE CASCADE, +FOREIGN KEY (launchpad_id) REFERENCES launchpads(id) ON DELETE CASCADE); \ No newline at end of file diff --git a/tools/collect_filesystem/docker-compose.yml b/tools/collect_filesystem/docker-compose.yml new file mode 100644 index 00000000..c9d57f91 --- /dev/null +++ b/tools/collect_filesystem/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3" +services: + app: + container_name: collect + build: . + links: + - db + ports: + - "5000:5000" + + db: + image: mysql:latest + container_name: collect_db + ports: + - "32000:3306" + environment: + MYSQL_ROOT_PASSWORD: Wind2019 + volumes: + - ./db:/docker-entrypoint-initdb.d/:ro \ No newline at end of file diff --git a/tools/collect_filesystem/setup package.zip b/tools/collect_filesystem/setup package.zip new file mode 100644 index 0000000000000000000000000000000000000000..d38d474793100065e457218b841e1293909285b6 GIT binary patch literal 23119 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 QgR3|es86G)FL7}G58~?2?EnA( literal 0 HcmV?d00001 diff --git a/tools/collect_filesystem/start.sh b/tools/collect_filesystem/start.sh new file mode 100644 index 00000000..aa14e6c7 --- /dev/null +++ b/tools/collect_filesystem/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker system prune +docker-compose up -d diff --git a/tools/collect_filesystem/update.sh b/tools/collect_filesystem/update.sh new file mode 100644 index 00000000..a3528503 --- /dev/null +++ b/tools/collect_filesystem/update.sh @@ -0,0 +1,3 @@ +#!/bin/bash +docker tag docker_app:latest nchen1windriver/collect:trail +docker push nchen1windriver/collect:trail