Repository: CTFd/CTFd

Issue:
Exporting Error
When I try to backup the CTFd data using the Admin Panel.I get an 500 error.And I'm using gunicorn to deploy it.
Here's the traceback:
```
~[2017-09-13 20:16:18,514] ERROR in app: Exception on /admin/export [GET]
Traceback (most recent call last):
  File "/usr/local/lib/python3.5/dist-packages/flask/app.py", line 1982, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python3.5/dist-packages/flask/app.py", line 1614, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python3.5/dist-packages/flask/app.py", line 1517, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python3.5/dist-packages/flask/_compat.py", line 33, in reraise
    raise value
  File "/usr/local/lib/python3.5/dist-packages/flask/app.py", line 1612, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python3.5/dist-packages/flask/app.py", line 1598, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/tiaonmmn/CTFd/CTFd/utils.py", line 224, in decorated_function
    return f(*args, **kwargs)
  File "/home/tiaonmmn/CTFd/CTFd/admin/__init__.py", line 82, in admin_export_ctf
    backup = export_ctf(segments.split(','))
  File "/home/tiaonmmn/CTFd/CTFd/utils.py", line 604, in export_ctf
    dataset.freeze(result, format='json', fileobj=result_file)
  File "/usr/local/lib/python3.5/dist-packages/dataset/freeze/app.py", line 125, in freeze
    return freeze_export(Export({}, kw), result=records)
  File "/usr/local/lib/python3.5/dist-packages/dataset/freeze/app.py", line 137, in freeze_export
    serializer.serialize()
  File "/usr/local/lib/python3.5/dist-packages/dataset/freeze/format/common.py", line 93, in serialize
    self.close()
  File "/usr/local/lib/python3.5/dist-packages/dataset/freeze/format/fjson.py", line 61, in close
    fh.write(data)
TypeError: a bytes-like object is required, not 'str'
```


Discussion:
Seems like a dataset glitch. I would need a copy of your database in order to replicate and test this. 
It looks like dataset the freezing functionality into https://github.com/pudo/datafreeze. This will get integrated into CTFd for 1.0.5

Resolving patch:
diff --git a/CTFd/utils.py b/CTFd/utils.py
--- a/CTFd/utils.py
+++ b/CTFd/utils.py
@@ -17,6 +17,7 @@
 import tempfile
 import time
 import dataset
+import datafreeze
 import zipfile
 import io
 
@@ -31,6 +32,9 @@
 
 from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, ip2long, long2ip
 
+from datafreeze.format import SERIALIZERS
+from datafreeze.format.fjson import JSONSerializer, JSONEncoder
+
 if six.PY2:
     text_type = unicode
     binary_type = str
@@ -45,6 +49,39 @@
 plugin_stylesheets = []
 
 
+class CTFdSerializer(JSONSerializer):
+    """
+    Slightly modified datafreeze serializer so that we can properly
+    export the CTFd database into a zip file.
+    """
+    def close(self):
+        for path, result in self.buckets.items():
+            result = self.wrap(result)
+
+            if self.fileobj is None:
+                fh = open(path, 'wb')
+            else:
+                fh = self.fileobj
+
+            data = json.dumps(result,
+                              cls=JSONEncoder,
+                              indent=self.export.get_int('indent'))
+
+            callback = self.export.get('callback')
+            if callback:
+                data = "%s && %s(%s);" % (callback, callback, data)
+
+            if six.PY3:
+                fh.write(bytes(data, encoding='utf-8'))
+            else:
+                fh.write(data)
+            if self.fileobj is None:
+                fh.close()
+
+
+SERIALIZERS['ctfd'] = CTFdSerializer  # Load the custom serializer
+
+
 def init_logs(app):
     logger_keys = logging.getLogger('keys')
     logger_logins = logging.getLogger('logins')
@@ -627,15 +664,16 @@ def export_ctf(segments=None):
     }
 
     # Backup database
-    backup = io.BytesIO()
+    backup = six.BytesIO()
+
     backup_zip = zipfile.ZipFile(backup, 'w')
 
     for segment in segments:
         group = groups[segment]
         for item in group:
             result = db[item].all()
-            result_file = io.BytesIO()
-            dataset.freeze(result, format='json', fileobj=result_file)
+            result_file = six.BytesIO()
+            datafreeze.freeze(result, format='ctfd', fileobj=result_file)
             result_file.seek(0)
             backup_zip.writestr('db/{}.json'.format(item), result_file.read())
 


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -4,6 +4,7 @@
 from sqlalchemy.engine.url import make_url
 import datetime
 import six
+import gc
 
 if six.PY2:
     text_type = unicode
@@ -42,6 +43,7 @@ def destroy_ctfd(app):
     with app.app_context():
         app.db.session.commit()
         app.db.session.close_all()
+        gc.collect()  # Garbage collect (necessary in the case of dataset freezes to clean database connections)
         app.db.drop_all()
     drop_database(app.config['SQLALCHEMY_DATABASE_URI'])
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -3,7 +3,7 @@
 
 from tests.helpers import *
 from CTFd.models import ip2long, long2ip
-from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended
+from CTFd.utils import get_config, set_config, override_template, sendmail, verify_email, ctf_started, ctf_ended, export_ctf
 from CTFd.utils import register_plugin_script, register_plugin_stylesheet
 from CTFd.utils import base64encode, base64decode
 from freezegun import freeze_time
@@ -324,3 +324,28 @@ def test_register_plugin_stylesheet():
             assert '/fake/stylesheet/path.css' in output
             assert 'http://ctfd.io/fake/stylesheet/path.css' in output
     destroy_ctfd(app)
+
+
+def test_export_ctf():
+    """Test that CTFd can properly export the database"""
+    app = create_ctfd()
+    with app.app_context():
+        register_user(app)
+        chal = gen_challenge(app.db, name=text_type('🐺'))
+        chal_id = chal.id
+        hint = gen_hint(app.db, chal_id)
+
+        client = login_as_user(app)
+        with client.session_transaction() as sess:
+            data = {
+                "nonce": sess.get('nonce')
+            }
+        r = client.post('/hints/1', data=data)
+        output = r.get_data(as_text=True)
+        output = json.loads(output)
+        app.db.session.commit()
+        backup = export_ctf()
+        backup.seek(0)
+        with open('export.zip', 'wb') as f:
+            f.write(backup.getvalue())
+    destroy_ctfd(app)