在一款Ffltter应用程序中,我正在try 将大型音频文件上传到围棋服务器,并将它们保存到Wasabi S3.
我可以看到我的服务器日志(log)为OPTIONS请求返回状态代码200,但没有POST请求和错误.在我的围棋服务器中,我使用chi路由,并添加了处理器头,以允许来自任何地方的CORS.我的应用程序很大,其他一切都运行得很好.授权承载令牌也在请求中传递.
我已经做出了禁用网络安全的更改,并在我的生产Ffltter服务器上测试了上传页面,在那里它也失败了.
OPTIONS请求接口日志(log)
"OPTIONS http://api.mydomain.com/transcript/upload/audio/file/2 HTTP/1.1" from 123.12.0.1:48558 - 200 0B in 39.061µs
以下是我可以从Fflight中收集到的错误
DioExceptionType.connectionError
https://api.mydomain.com/transcript/upload/audio/file/2
https://api.mydomain.com/transcript/upload/audio/file/2
{Content-Type: application/octet-stream, Authorization: Bearer my-token, Access-Control-Allow-Origin: *}
相关接口代码
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300, // Maximum value not ignored by any of major browsers
}))
const maxUploadSize = 500 * 1024 * 1024 // 500 MB
id := chi.URLParam(r, "transcriptId")
transcriptId, err := strconv.ParseInt(id, 10, 64)
if err := r.ParseMultipartForm(32 << 20); err != nil {
log.Println(err)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
file, handler, err := r.FormFile("file")
Flutter 上传码
class FileUploadView extends StatefulWidget {
const FileUploadView({super.key});
@override
State<FileUploadView> createState() => _FileUploadViewState();
}
class _FileUploadViewState extends State<FileUploadView> {
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback(
(_) => showSnackBar(context),
);
super.initState();
}
FilePickerResult? result;
PlatformFile? file;
Response? response;
String? progress;
Dio dio = Dio();
String success = 'Your file was uploaded successfully';
String failure = 'Your file could not be uploaded';
bool replaceFile = false;
selectFile() async {
FilePickerResult? result = await FilePicker.platform
.pickFiles(type: FileType.any, withReadStream: true);
if (result != null) {
file = result.files.single;
}
setState(() {});
}
Future<void> uploadFile(BuildContext context, User user) async {
final navigator = Navigator.of(context);
const storage = FlutterSecureStorage();
String? token = await storage.read(key: 'jwt');
dio.options.headers['Content-Type'] = 'application/octet-stream';
dio.options.headers["Authorization"] = "Bearer $token";
dio.options.headers['Access-Control-Allow-Origin'] = '*';
dio.options.baseUrl = user.fileUrl;
final uploader = ChunkedUploader(dio);
try {
response = await uploader.upload(
fileKey: 'file',
method: 'POST',
fileName: file!.name,
fileSize: file!.size,
fileDataStream: file!.readStream!,
maxChunkSize: 32000000,
path: user.fileUrl,
onUploadProgress: (progress) => setState(
() {
progress;
},
),
);
if (response!.statusCode == 200) {
user.snackBarType = SnackBarType.success;
user.snackBarMessage = success;
navigator.pushNamedAndRemoveUntil(
RoutePaths.matterTabs, (route) => false);
} else {
user.snackBarType = SnackBarType.failure;
user.snackBarMessage = failure;
navigator.pushNamedAndRemoveUntil(
RoutePaths.matterTabs, (route) => false);
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
print('status code 404');
} else {
print(e.message ?? 'no error message available');
print(e.requestOptions.toString());
print(e.response.toString());
print(e.type.toString());
print(user.fileUrl);
print(dio.options.baseUrl);
print(dio.options.headers);
}
}
}
@override
Widget build(BuildContext context) {
User user = Provider.of<User>(context, listen: false);
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
centerTitle: true,
title: Text(
'app name',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => {
Navigator.of(context).pushNamedAndRemoveUntil(
RoutePaths.matterTabs, (route) => false)
},
),
),
body: Container(
padding: const EdgeInsets.all(12.0),
child: SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 12),
const Text(
'Select and Upload File',
maxLines: 4,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
softWrap: true,
),
const SizedBox(height: 24),
Container(
margin: const EdgeInsets.all(10),
//show file name here
child: progress == null
? const Text("Progress: 0%")
: Text(
"Progress: $progress",
textAlign: TextAlign.center,
),
//show progress status here
),
const SizedBox(height: 24),
Container(
margin: const EdgeInsets.all(10),
//show file name here
child: file == null
? const Text(
'Choose File',
)
: Text(
file!.name,
),
//basename is from path package, to get filename from path
//check if file is selected, if yes then show file name
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () async {
selectFile();
},
icon: const Icon(Icons.folder_open),
label: const Text(
"CHOOSE FILE",
),
),
const SizedBox(height: 24),
//if selectedfile is null then show empty container
//if file is selected then show upload button
file == null
? Container()
: ElevatedButton.icon(
onPressed: () async {
if (user.fileExists) {
_replaceExistingFile(context, user);
} else {
uploadFile(context, user);
}
},
icon: const Icon(Icons.upload),
label: const Text(
"UPLOAD FILE",
),
),
],
),
),
),
),
);
}
_replaceExistingFile(BuildContext context, User user) {
bool firstPress = true;
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('File Exists'),
content: Text("Do you want to replace ${user.uploadFileName}?"),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
{
Navigator.of(context).pop(false);
}
},
),
TextButton(
child: const Text('Replace'),
onPressed: () async {
if (firstPress) {
firstPress = false;
{
uploadFile(context, user);
}
} else {}
},
)
],
);
},
);
}
}