cecilia-uu commited on
Commit
de7c780
·
1 Parent(s): 70b1cb8

API: upload document api (#1264)

Browse files

### What problem does this PR solve?

API: Adds the feature of uploading document.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

api/apps/dataset_api.py CHANGED
@@ -14,36 +14,23 @@
14
  # limitations under the License.
15
 
16
 
17
- import json
18
- import os
19
- import re
20
- from datetime import datetime, timedelta
21
- from flask import request, Response
22
  from flask_login import login_required, current_user
23
  from httpx import HTTPError
24
 
25
- from api.db import FileType, ParserType, FileSource, StatusEnum
26
- from api.db.db_models import APIToken, API4Conversation, Task, File
 
27
  from api.db.services import duplicate_name
28
- from api.db.services.api_service import APITokenService, API4ConversationService
29
- from api.db.services.dialog_service import DialogService, chat
30
  from api.db.services.document_service import DocumentService
31
  from api.db.services.file2document_service import File2DocumentService
32
  from api.db.services.file_service import FileService
33
  from api.db.services.knowledgebase_service import KnowledgebaseService
34
- from api.db.services.task_service import queue_tasks, TaskService
35
- from api.db.services.user_service import UserTenantService, TenantService
36
- from api.settings import RetCode, retrievaler
37
- from api.utils import get_uuid, current_timestamp, datetime_format
38
- # from api.utils.api_utils import server_error_response, get_data_error_result, get_json_result, validate_request
39
- from itsdangerous import URLSafeTimedSerializer
40
-
41
- from api.utils.file_utils import filename_type, thumbnail
42
- from rag.utils.minio_conn import MINIO
43
-
44
- # import library
45
  from api.utils.api_utils import construct_json_result, construct_result, construct_error_response, validate_request
46
- from api.contants import NAME_LENGTH_LIMIT
47
 
48
  # ------------------------------ create a dataset ---------------------------------------
49
 
 
14
  # limitations under the License.
15
 
16
 
17
+ from flask import request
 
 
 
 
18
  from flask_login import login_required, current_user
19
  from httpx import HTTPError
20
 
21
+ from api.contants import NAME_LENGTH_LIMIT
22
+ from api.db import FileSource, StatusEnum
23
+ from api.db.db_models import File
24
  from api.db.services import duplicate_name
 
 
25
  from api.db.services.document_service import DocumentService
26
  from api.db.services.file2document_service import File2DocumentService
27
  from api.db.services.file_service import FileService
28
  from api.db.services.knowledgebase_service import KnowledgebaseService
29
+ from api.db.services.user_service import TenantService
30
+ from api.settings import RetCode
31
+ from api.utils import get_uuid
 
 
 
 
 
 
 
 
32
  from api.utils.api_utils import construct_json_result, construct_result, construct_error_response, validate_request
33
+
34
 
35
  # ------------------------------ create a dataset ---------------------------------------
36
 
api/apps/documents_api.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #
2
+ # Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License
15
+ #
16
+
17
+ import os
18
+ import re
19
+ import warnings
20
+
21
+ from flask import request
22
+ from flask_login import login_required, current_user
23
+
24
+ from api.db import FileType, ParserType
25
+ from api.db.services import duplicate_name
26
+ from api.db.services.document_service import DocumentService
27
+ from api.db.services.file_service import FileService
28
+ from api.db.services.knowledgebase_service import KnowledgebaseService
29
+ from api.settings import RetCode
30
+ from api.utils import get_uuid
31
+ from api.utils.api_utils import construct_json_result
32
+ from api.utils.file_utils import filename_type, thumbnail
33
+ from rag.utils.minio_conn import MINIO
34
+
35
+
36
+ MAXIMUM_OF_UPLOADING_FILES = 256
37
+
38
+
39
+ # ----------------------------upload local files-----------------------------------------------------
40
+ @manager.route('/<dataset_id>', methods=['POST'])
41
+ @login_required
42
+ def upload(dataset_id):
43
+ # no files
44
+ if not request.files:
45
+ return construct_json_result(
46
+ message='There is no file!', code=RetCode.ARGUMENT_ERROR)
47
+
48
+ # the number of uploading files exceeds the limit
49
+ file_objs = request.files.getlist('file')
50
+ num_file_objs = len(file_objs)
51
+
52
+ if num_file_objs > MAXIMUM_OF_UPLOADING_FILES:
53
+ return construct_json_result(code=RetCode.DATA_ERROR, message=f"You try to upload {num_file_objs} files, "
54
+ f"which exceeds the maximum number of uploading files: {MAXIMUM_OF_UPLOADING_FILES}")
55
+
56
+ for file_obj in file_objs:
57
+ # the content of the file
58
+ file_content = file_obj.read()
59
+ file_name = file_obj.filename
60
+ # no name
61
+ if not file_name:
62
+ return construct_json_result(
63
+ message='There is a file without name!', code=RetCode.ARGUMENT_ERROR)
64
+
65
+ # TODO: support the remote files
66
+ if 'http' in file_name:
67
+ return construct_json_result(code=RetCode.ARGUMENT_ERROR, message="Remote files have not unsupported.")
68
+
69
+ # the content is empty, raising a warning
70
+ if file_content == b'':
71
+ warnings.warn(f"[WARNING]: The file {file_name} is empty.")
72
+
73
+ # no dataset
74
+ exist, dataset = KnowledgebaseService.get_by_id(dataset_id)
75
+ if not exist:
76
+ return construct_json_result(message="Can't find this dataset", code=RetCode.DATA_ERROR)
77
+
78
+ # get the root_folder
79
+ root_folder = FileService.get_root_folder(current_user.id)
80
+ # get the id of the root_folder
81
+ parent_file_id = root_folder["id"] # document id
82
+ # this is for the new user, create '.knowledgebase' file
83
+ FileService.init_knowledgebase_docs(parent_file_id, current_user.id)
84
+ # go inside this folder, get the kb_root_folder
85
+ kb_root_folder = FileService.get_kb_folder(current_user.id)
86
+ # link the file management to the kb_folder
87
+ kb_folder = FileService.new_a_file_from_kb(dataset.tenant_id, dataset.name, kb_root_folder["id"])
88
+
89
+ # grab all the errs
90
+ err = []
91
+ MAX_FILE_NUM_PER_USER = int(os.environ.get('MAX_FILE_NUM_PER_USER', 0))
92
+ for file in file_objs:
93
+ try:
94
+ # TODO: get this value from the database as some tenants have this limit while others don't
95
+ if MAX_FILE_NUM_PER_USER > 0 and DocumentService.get_doc_count(dataset.tenant_id) >= MAX_FILE_NUM_PER_USER:
96
+ return construct_json_result(code=RetCode.DATA_ERROR,
97
+ message="Exceed the maximum file number of a free user!")
98
+ # deal with the duplicate name
99
+ filename = duplicate_name(
100
+ DocumentService.query,
101
+ name=file.filename,
102
+ kb_id=dataset.id)
103
+
104
+ # deal with the unsupported type
105
+ filetype = filename_type(filename)
106
+ if filetype == FileType.OTHER.value:
107
+ return construct_json_result(code=RetCode.DATA_ERROR,
108
+ message="This type of file has not been supported yet!")
109
+
110
+ # upload to the minio
111
+ location = filename
112
+ while MINIO.obj_exist(dataset_id, location):
113
+ location += "_"
114
+ blob = file.read()
115
+ MINIO.put(dataset_id, location, blob)
116
+ doc = {
117
+ "id": get_uuid(),
118
+ "kb_id": dataset.id,
119
+ "parser_id": dataset.parser_id,
120
+ "parser_config": dataset.parser_config,
121
+ "created_by": current_user.id,
122
+ "type": filetype,
123
+ "name": filename,
124
+ "location": location,
125
+ "size": len(blob),
126
+ "thumbnail": thumbnail(filename, blob)
127
+ }
128
+ if doc["type"] == FileType.VISUAL:
129
+ doc["parser_id"] = ParserType.PICTURE.value
130
+ if re.search(r"\.(ppt|pptx|pages)$", filename):
131
+ doc["parser_id"] = ParserType.PRESENTATION.value
132
+ DocumentService.insert(doc)
133
+
134
+ FileService.add_file_from_kb(doc, kb_folder["id"], dataset.tenant_id)
135
+ except Exception as e:
136
+ err.append(file.filename + ": " + str(e))
137
+
138
+ if err:
139
+ # return all the errors
140
+ return construct_json_result(message="\n".join(err), code=RetCode.SERVER_ERROR)
141
+ # success
142
+ return construct_json_result(data=True, code=RetCode.SUCCESS)
143
+
144
+ # ----------------------------upload online files------------------------------------------------
145
+
146
+ # ----------------------------download a file-----------------------------------------------------
147
+
148
+ # ----------------------------delete a file-----------------------------------------------------
149
+
150
+ # ----------------------------enable rename-----------------------------------------------------
151
+
152
+ # ----------------------------list files-----------------------------------------------------
153
+
154
+ # ----------------------------start parsing-----------------------------------------------------
155
+
156
+ # ----------------------------stop parsing-----------------------------------------------------
157
+
158
+ # ----------------------------show the status of the file-----------------------------------------------------
159
+
160
+ # ----------------------------list the chunks of the file-----------------------------------------------------
161
+
162
+ # ----------------------------delete the chunk-----------------------------------------------------
163
+
164
+ # ----------------------------edit the status of the chunk-----------------------------------------------------
165
+
166
+ # ----------------------------insert a new chunk-----------------------------------------------------
167
+
168
+ # ----------------------------upload a file-----------------------------------------------------
169
+
170
+ # ----------------------------get a specific chunk-----------------------------------------------------
171
+
172
+ # ----------------------------retrieval test-----------------------------------------------------
sdk/python/ragflow/ragflow.py CHANGED
@@ -13,9 +13,12 @@
13
  # See the License for the specific language governing permissions and
14
  # limitations under the License.
15
 
 
16
  import os
 
17
  import requests
18
- import json
 
19
 
20
 
21
  class RAGFlow:
@@ -23,10 +26,12 @@ class RAGFlow:
23
  '''
24
  api_url: http://<host_address>/api/v1
25
  dataset_url: http://<host_address>/api/v1/dataset
 
26
  '''
27
  self.user_key = user_key
28
  self.api_url = f"{base_url}/api/{version}"
29
  self.dataset_url = f"{self.api_url}/dataset"
 
30
  self.authorization_header = {"Authorization": "{}".format(self.user_key)}
31
 
32
  def create_dataset(self, dataset_name):
@@ -73,3 +78,54 @@ class RAGFlow:
73
  endpoint = f"{self.dataset_url}/{dataset_id}"
74
  response = requests.put(endpoint, json=params, headers=self.authorization_header)
75
  return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  # See the License for the specific language governing permissions and
14
  # limitations under the License.
15
 
16
+ import json
17
  import os
18
+
19
  import requests
20
+
21
+ from api.settings import RetCode
22
 
23
 
24
  class RAGFlow:
 
26
  '''
27
  api_url: http://<host_address>/api/v1
28
  dataset_url: http://<host_address>/api/v1/dataset
29
+ document_url: http://<host_address>/api/v1/documents
30
  '''
31
  self.user_key = user_key
32
  self.api_url = f"{base_url}/api/{version}"
33
  self.dataset_url = f"{self.api_url}/dataset"
34
+ self.document_url = f"{self.api_url}/documents"
35
  self.authorization_header = {"Authorization": "{}".format(self.user_key)}
36
 
37
  def create_dataset(self, dataset_name):
 
78
  endpoint = f"{self.dataset_url}/{dataset_id}"
79
  response = requests.put(endpoint, json=params, headers=self.authorization_header)
80
  return response.json()
81
+
82
+ # -------------------- content management -----------------------------------------------------
83
+
84
+ # ----------------------------upload local files-----------------------------------------------------
85
+ def upload_local_file(self, dataset_id, file_paths):
86
+ files = []
87
+
88
+ for file_path in file_paths:
89
+ if not isinstance(file_path, str):
90
+ return {'code': RetCode.ARGUMENT_ERROR, 'message': f"{file_path} is not string."}
91
+ if 'http' in file_path:
92
+ return {'code': RetCode.ARGUMENT_ERROR, 'message': "Remote files have not unsupported."}
93
+ if os.path.isfile(file_path):
94
+ files.append(('file', open(file_path, 'rb')))
95
+ else:
96
+ return {'code': RetCode.DATA_ERROR, 'message': f"The file {file_path} does not exist"}
97
+
98
+ res = requests.request('POST', url=f"{self.document_url}/{dataset_id}", files=files,
99
+ headers=self.authorization_header)
100
+
101
+ result_dict = json.loads(res.text)
102
+ return result_dict
103
+
104
+ # ----------------------------upload remote files-----------------------------------------------------
105
+ # ----------------------------download a file-----------------------------------------------------
106
+
107
+ # ----------------------------delete a file-----------------------------------------------------
108
+
109
+ # ----------------------------enable rename-----------------------------------------------------
110
+
111
+ # ----------------------------list files-----------------------------------------------------
112
+
113
+ # ----------------------------start parsing-----------------------------------------------------
114
+
115
+ # ----------------------------stop parsing-----------------------------------------------------
116
+
117
+ # ----------------------------show the status of the file-----------------------------------------------------
118
+
119
+ # ----------------------------list the chunks of the file-----------------------------------------------------
120
+
121
+ # ----------------------------delete the chunk-----------------------------------------------------
122
+
123
+ # ----------------------------edit the status of the chunk-----------------------------------------------------
124
+
125
+ # ----------------------------insert a new chunk-----------------------------------------------------
126
+
127
+ # ----------------------------upload a file-----------------------------------------------------
128
+
129
+ # ----------------------------get a specific chunk-----------------------------------------------------
130
+
131
+ # ----------------------------retrieval test-----------------------------------------------------
sdk/python/test/test_data/.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ hhh
2
+ hhh
sdk/python/test/test_data/empty.txt ADDED
File without changes
sdk/python/test/test_data/test.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ test
2
+ test
3
+ test
sdk/python/test/test_data/test1.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ test1
2
+ test1
sdk/python/test/test_document.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from api.settings import RetCode
2
+ from test_sdkbase import TestSdk
3
+ from ragflow import RAGFlow
4
+ import pytest
5
+ from common import API_KEY, HOST_ADDRESS
6
+ from api.contants import NAME_LENGTH_LIMIT
7
+
8
+
9
+ class TestFile(TestSdk):
10
+ """
11
+ This class contains a suite of tests for the content management functionality within the dataset.
12
+ It ensures that the following functionalities as expected:
13
+ 1. upload local files
14
+ 2. upload remote files
15
+ 3. download a file
16
+ 4. delete a file
17
+ 5. enable rename
18
+ 6. list files
19
+ 7. start parsing
20
+ 8. end parsing
21
+ 9. check the status of the file
22
+ 10. list the chunks
23
+ 11. delete a chunk
24
+ 12. insert a new chunk
25
+ 13. edit the status of chunk
26
+ 14. get the specific chunk
27
+ 15. retrieval test
28
+ """
29
+
30
+ # ----------------------------upload local files-----------------------------------------------------
31
+ def test_upload_two_files(self):
32
+ """
33
+ Test uploading two files with success.
34
+ """
35
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
36
+ created_res = ragflow.create_dataset("test_upload_two_files")
37
+ dataset_id = created_res['data']['dataset_id']
38
+ file_paths = ["test_data/test.txt", "test_data/test1.txt"]
39
+ res = ragflow.upload_local_file(dataset_id, file_paths)
40
+ assert res['code'] == RetCode.SUCCESS and res['data'] is True and res['message'] == 'success'
41
+
42
+ def test_upload_one_file(self):
43
+ """
44
+ Test uploading one file with success.
45
+ """
46
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
47
+ created_res = ragflow.create_dataset("test_upload_one_file")
48
+ dataset_id = created_res['data']['dataset_id']
49
+ file_paths = ["test_data/test.txt"]
50
+ res = ragflow.upload_local_file(dataset_id, file_paths)
51
+ assert res['code'] == RetCode.SUCCESS and res['data'] is True and res['message'] == 'success'
52
+
53
+ def test_upload_nonexistent_files(self):
54
+ """
55
+ Test uploading a file which does not exist.
56
+ """
57
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
58
+ created_res = ragflow.create_dataset("test_upload_nonexistent_files")
59
+ dataset_id = created_res['data']['dataset_id']
60
+ file_paths = ["test_data/imagination.txt"]
61
+ res = ragflow.upload_local_file(dataset_id, file_paths)
62
+ assert res['code'] == RetCode.DATA_ERROR and "does not exist" in res['message']
63
+
64
+ def test_upload_file_if_dataset_does_not_exist(self):
65
+ """
66
+ Test uploading files if the dataset id does not exist.
67
+ """
68
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
69
+ file_paths = ["test_data/test.txt"]
70
+ res = ragflow.upload_local_file("111", file_paths)
71
+ assert res['code'] == RetCode.DATA_ERROR and res['message'] == "Can't find this dataset"
72
+
73
+ def test_upload_file_without_name(self):
74
+ """
75
+ Test uploading files that do not have name.
76
+ """
77
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
78
+ created_res = ragflow.create_dataset("test_upload_file_without_name")
79
+ dataset_id = created_res['data']['dataset_id']
80
+ file_paths = ["test_data/.txt"]
81
+ res = ragflow.upload_local_file(dataset_id, file_paths)
82
+ assert res['code'] == RetCode.SUCCESS
83
+
84
+ def test_upload_file_without_name1(self):
85
+ """
86
+ Test uploading files that do not have name.
87
+ """
88
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
89
+ created_res = ragflow.create_dataset("test_upload_file_without_name")
90
+ dataset_id = created_res['data']['dataset_id']
91
+ file_paths = ["test_data/.txt", "test_data/empty.txt"]
92
+ res = ragflow.upload_local_file(dataset_id, file_paths)
93
+ assert res['code'] == RetCode.SUCCESS
94
+
95
+ def test_upload_files_exceeding_the_number_limit(self):
96
+ """
97
+ Test uploading files whose number exceeds the limit.
98
+ """
99
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
100
+ created_res = ragflow.create_dataset("test_upload_files_exceeding_the_number_limit")
101
+ dataset_id = created_res['data']['dataset_id']
102
+ file_paths = ["test_data/test.txt", "test_data/test1.txt"] * 256
103
+ res = ragflow.upload_local_file(dataset_id, file_paths)
104
+ assert (res['message'] ==
105
+ 'You try to upload 512 files, which exceeds the maximum number of uploading files: 256'
106
+ and res['code'] == RetCode.DATA_ERROR)
107
+
108
+ def test_upload_files_without_files(self):
109
+ """
110
+ Test uploading files without files.
111
+ """
112
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
113
+ created_res = ragflow.create_dataset("test_upload_files_without_files")
114
+ dataset_id = created_res['data']['dataset_id']
115
+ file_paths = [None]
116
+ res = ragflow.upload_local_file(dataset_id, file_paths)
117
+ assert (res['message'] == 'None is not string.' and res['code'] == RetCode.ARGUMENT_ERROR)
118
+
119
+ def test_upload_files_with_two_files_with_same_name(self):
120
+ """
121
+ Test uploading files with the same name.
122
+ """
123
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
124
+ created_res = ragflow.create_dataset("test_upload_files_with_two_files_with_same_name")
125
+ dataset_id = created_res['data']['dataset_id']
126
+ file_paths = ['test_data/test.txt'] * 2
127
+ res = ragflow.upload_local_file(dataset_id, file_paths)
128
+ assert (res['message'] == 'success' and res['code'] == RetCode.SUCCESS)
129
+
130
+ def test_upload_files_with_file_paths(self):
131
+ """
132
+ Test uploading files with only specifying the file path's repo.
133
+ """
134
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
135
+ created_res = ragflow.create_dataset("test_upload_files_with_file_paths")
136
+ dataset_id = created_res['data']['dataset_id']
137
+ file_paths = ['test_data/']
138
+ res = ragflow.upload_local_file(dataset_id, file_paths)
139
+ assert (res['message'] == 'The file test_data/ does not exist' and res['code'] == RetCode.DATA_ERROR)
140
+
141
+ def test_upload_files_with_remote_file_path(self):
142
+ """
143
+ Test uploading files with remote files.
144
+ """
145
+ ragflow = RAGFlow(API_KEY, HOST_ADDRESS)
146
+ created_res = ragflow.create_dataset("test_upload_files_with_remote_file_path")
147
+ dataset_id = created_res['data']['dataset_id']
148
+ file_paths = ['https://github.com/genostack/ragflow']
149
+ res = ragflow.upload_local_file(dataset_id, file_paths)
150
+ assert res['code'] == RetCode.ARGUMENT_ERROR and res['message'] == 'Remote files have not unsupported.'
151
+
152
+ # ----------------------------upload remote files-----------------------------------------------------
153
+
154
+ # ----------------------------download a file-----------------------------------------------------
155
+
156
+ # ----------------------------delete a file-----------------------------------------------------
157
+
158
+ # ----------------------------enable rename-----------------------------------------------------
159
+
160
+ # ----------------------------list files-----------------------------------------------------
161
+
162
+ # ----------------------------start parsing-----------------------------------------------------
163
+
164
+ # ----------------------------stop parsing-----------------------------------------------------
165
+
166
+ # ----------------------------show the status of the file-----------------------------------------------------
167
+
168
+ # ----------------------------list the chunks of the file-----------------------------------------------------
169
+
170
+ # ----------------------------delete the chunk-----------------------------------------------------
171
+
172
+ # ----------------------------edit the status of the chunk-----------------------------------------------------
173
+
174
+ # ----------------------------insert a new chunk-----------------------------------------------------
175
+
176
+ # ----------------------------upload a file-----------------------------------------------------
177
+
178
+ # ----------------------------get a specific chunk-----------------------------------------------------
179
+
180
+ # ----------------------------retrieval test-----------------------------------------------------