zsw
4 months ago
49 changed files with 3840 additions and 0 deletions
@ -0,0 +1,58 @@
|
||||
language: dart |
||||
|
||||
dart: |
||||
- stable |
||||
|
||||
install: |
||||
- cd $TRAVIS_BUILD_DIR/base && pub get |
||||
# - cd $$TRAVIS_BUILD_DIR/flutter && pub get |
||||
|
||||
before_script: |
||||
- cd $TRAVIS_BUILD_DIR/base && rm -rf .env |
||||
|
||||
jobs: |
||||
include: |
||||
####################################### |
||||
######### jobs for base ############# |
||||
####################################### |
||||
# 检查 lint 并在 warnings、infos 时报错退出 |
||||
- stage: base(analyze,format,test) |
||||
name: "Analyze" |
||||
os: linux |
||||
script: cd $TRAVIS_BUILD_DIR/base && dartanalyzer --fatal-warnings --fatal-infos . |
||||
# 检查格式并在异常时退出 |
||||
- stage: base(analyze,format,test) |
||||
name: "Format" |
||||
os: linux |
||||
script: cd $TRAVIS_BUILD_DIR/base && dartfmt -n --set-exit-if-changed . |
||||
# 执行测试(已开启 null safe) |
||||
- stage: base(analyze,format,test) |
||||
name: "Vm Tests" |
||||
os: linux |
||||
script: cd $TRAVIS_BUILD_DIR/base && pub run test_coverage --print-test-output && bash <(curl -s https://codecov.io/bash) |
||||
|
||||
####################################### |
||||
####### jobs for flutter_sdk ########## |
||||
####################################### |
||||
# - stage: flutter_sdk(analyze,format,test) |
||||
# name: "Analyze" |
||||
# os: linux |
||||
# script: cd $TRAVIS_BUILD_DIR/flutter |
||||
# - stage: flutter_sdk(analyze,format,test) |
||||
# name: "Format" |
||||
# os: linux |
||||
# script: cd $TRAVIS_BUILD_DIR/flutter |
||||
|
||||
# - stage: flutter_sdk(analyze,format,test) |
||||
# name: "Vm Tests" |
||||
# os: linux |
||||
# script: cd $TRAVIS_BUILD_DIR/flutter |
||||
|
||||
stages: |
||||
- base(analyze,format,test) |
||||
# - flutter_sdk(analyze,format,test) |
||||
# - flutter_sdk_example(analyze,format,test) |
||||
|
||||
cache: |
||||
directories: |
||||
- $HOME/.pub-cache |
@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct |
||||
|
||||
## Our Pledge |
||||
|
||||
In the interest of fostering an open and welcoming environment, we as |
||||
contributors and maintainers pledge to making participation in our project and |
||||
our community a harassment-free experience for everyone, regardless of age, body |
||||
size, disability, ethnicity, sex characteristics, gender identity and expression, |
||||
level of experience, education, socio-economic status, nationality, personal |
||||
appearance, race, religion, or sexual identity and orientation. |
||||
|
||||
## Our Standards |
||||
|
||||
Examples of behavior that contributes to creating a positive environment |
||||
include: |
||||
|
||||
* Using welcoming and inclusive language |
||||
* Being respectful of differing viewpoints and experiences |
||||
* Gracefully accepting constructive criticism |
||||
* Focusing on what is best for the community |
||||
* Showing empathy towards other community members |
||||
|
||||
Examples of unacceptable behavior by participants include: |
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or |
||||
advances |
||||
* Trolling, insulting/derogatory comments, and personal or political attacks |
||||
* Public or private harassment |
||||
* Publishing others' private information, such as a physical or electronic |
||||
address, without explicit permission |
||||
* Other conduct which could reasonably be considered inappropriate in a |
||||
professional setting |
||||
|
||||
## Our Responsibilities |
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable |
||||
behavior and are expected to take appropriate and fair corrective action in |
||||
response to any instances of unacceptable behavior. |
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or |
||||
reject comments, commits, code, wiki edits, issues, and other contributions |
||||
that are not aligned to this Code of Conduct, or to ban temporarily or |
||||
permanently any contributor for other behaviors that they deem inappropriate, |
||||
threatening, offensive, or harmful. |
||||
|
||||
## Scope |
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces |
||||
when an individual is representing the project or its community. Examples of |
||||
representing a project or community include using an official project e-mail |
||||
address, posting via an official social media account, or acting as an appointed |
||||
representative at an online or offline event. Representation of a project may be |
||||
further defined and clarified by project maintainers. |
||||
|
||||
## Enforcement |
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
||||
reported by contacting the project team at yinxulai@qiniu.com. All |
||||
complaints will be reviewed and investigated and will result in a response that |
||||
is deemed necessary and appropriate to the circumstances. The project team is |
||||
obligated to maintain confidentiality with regard to the reporter of an incident. |
||||
Further details of specific enforcement policies may be posted separately. |
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good |
||||
faith may face temporary or permanent repercussions as determined by other |
||||
members of the project's leadership. |
||||
|
||||
## Attribution |
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
||||
|
||||
[homepage]: https://www.contributor-covenant.org |
||||
|
||||
For answers to common questions about this code of conduct, see |
||||
https://www.contributor-covenant.org/faq |
@ -0,0 +1,15 @@
|
||||
# Dart SDK |
||||
|
||||
[![codecov](https://codecov.io/gh/qiniu/dart-sdk/branch/master/graph/badge.svg?token=5VOX6NJTKF)](https://codecov.io/gh/qiniu/dart-sdk) |
||||
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) |
||||
[![qiniu_sdk_base](https://img.shields.io/pub/v/qiniu_sdk_base.svg?label=qiniu_sdk_base)](https://pub.dev/packages/qiniu_sdk_base) |
||||
[![qiniu_flutter_sdk](https://img.shields.io/pub/v/qiniu_flutter_sdk.svg?label=qiniu_flutter_sdk)](https://pub.dev/packages/qiniu_flutter_sdk) |
||||
|
||||
## 目录说明 |
||||
|
||||
- base 封装了七牛各业务的基础实现 |
||||
- flutter 该目录是 base + Flutter 的绑定实现,同时导出为单独的 package 提供给用户使用 |
||||
|
||||
### [Flutter SDK](https://github.com/qiniu/dart-sdk/tree/master/flutter) |
||||
|
||||
七牛云业务基于 Dart 绑定 Flutter 的实现,为 Flutter 提供简易的使用方式,更多信息查看该目录下的 [README.md](https://github.com/qiniu/dart-sdk/tree/master/flutter/README.md) 文件。 |
@ -0,0 +1,12 @@
|
||||
## 0.1.0 |
||||
|
||||
- Initial Release. |
||||
|
||||
## 0.2.0 |
||||
|
||||
- 优化了 `StorageError` 输出的调用栈 |
||||
- `CacheProvider` 的方法都改成异步的 |
||||
|
||||
## 0.2.1 |
||||
|
||||
- 修复关闭 App 缓存丢失的问题 |
@ -0,0 +1,201 @@
|
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright [yyyy] [name of copyright owner] |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
you may not use this file except in compliance with the License. |
||||
You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software |
||||
distributed under the License is distributed on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
See the License for the specific language governing permissions and |
||||
limitations under the License. |
@ -0,0 +1,29 @@
|
||||
# Qiniu Sdk Base [![qiniu_sdk_base](https://img.shields.io/pub/v/qiniu_sdk_base.svg?label=qiniu_sdk_base)](https://pub.dev/packages/qiniu_sdk_base) [![codecov](https://codecov.io/gh/qiniu/dart-sdk/branch/master/graph/badge.svg?token=5VOX6NJTKF)](https://codecov.io/gh/qiniu/dart-sdk) |
||||
|
||||
七牛 dart 平台 sdk 的 base 包,为上层 sdk 提供基础设施和共享代码。 |
||||
|
||||
## 功能列表 |
||||
|
||||
* 单文件上传 |
||||
* 分片上传 |
||||
* 任务状态 |
||||
* 任务进度 |
||||
* 上传进度 |
||||
* 失败重试 |
||||
|
||||
## 如何测试 |
||||
|
||||
创建 `.env` 文件,并输入如下内容 |
||||
|
||||
``` |
||||
export QINIU_DART_SDK_ACCESS_KEY= |
||||
export QINIU_DART_SDK_SECRET_KEY= |
||||
export QINIU_DART_SDK_TOKEN_SCOPE= |
||||
``` |
||||
|
||||
|
||||
在 `.env` 文件中填好敏感数据,即 ak、sk、scope |
||||
|
||||
接着运行如下指令 |
||||
|
||||
`pub run test` |
@ -0,0 +1,90 @@
|
||||
# copy from https://github.com/dart-lang/http/blob/master/analysis_options.yaml |
||||
|
||||
include: package:pedantic/analysis_options.yaml |
||||
|
||||
analyzer: |
||||
# enable-experiment: |
||||
# - non-nullable |
||||
|
||||
strong-mode: |
||||
implicit-casts: false |
||||
implicit-dynamic: false |
||||
|
||||
linter: |
||||
rules: |
||||
- annotate_overrides |
||||
- avoid_bool_literals_in_conditional_expressions |
||||
- avoid_classes_with_only_static_members |
||||
- avoid_empty_else |
||||
- avoid_function_literals_in_foreach_calls |
||||
- avoid_init_to_null |
||||
- avoid_null_checks_in_equality_operators |
||||
- avoid_relative_lib_imports |
||||
- avoid_renaming_method_parameters |
||||
- avoid_return_types_on_setters |
||||
- avoid_returning_null_for_void |
||||
- avoid_returning_this |
||||
- avoid_shadowing_type_parameters |
||||
- avoid_single_cascade_in_expression_statements |
||||
- avoid_types_as_parameter_names |
||||
- avoid_unused_constructor_parameters |
||||
- await_only_futures |
||||
- camel_case_types |
||||
- cascade_invocations |
||||
# comment_references |
||||
- control_flow_in_finally |
||||
- curly_braces_in_flow_control_structures |
||||
- directives_ordering |
||||
- empty_catches |
||||
- empty_constructor_bodies |
||||
- empty_statements |
||||
- file_names |
||||
- hash_and_equals |
||||
- invariant_booleans |
||||
- iterable_contains_unrelated_type |
||||
- library_names |
||||
- library_prefixes |
||||
- list_remove_unrelated_type |
||||
- no_adjacent_strings_in_list |
||||
- no_duplicate_case_values |
||||
- non_constant_identifier_names |
||||
- null_closures |
||||
- omit_local_variable_types |
||||
- only_throw_errors |
||||
- overridden_fields |
||||
- package_names |
||||
- package_prefixed_library_names |
||||
- prefer_adjacent_string_concatenation |
||||
- prefer_conditional_assignment |
||||
- prefer_contains |
||||
- prefer_equal_for_default_values |
||||
- prefer_final_fields |
||||
- prefer_collection_literals |
||||
- prefer_generic_function_type_aliases |
||||
- prefer_initializing_formals |
||||
- prefer_is_empty |
||||
- prefer_is_not_empty |
||||
- prefer_null_aware_operators |
||||
- prefer_single_quotes |
||||
- prefer_typing_uninitialized_variables |
||||
- recursive_getters |
||||
- slash_for_doc_comments |
||||
- test_types_in_equals |
||||
- throw_in_finally |
||||
- type_init_formals |
||||
- unawaited_futures |
||||
- unnecessary_brace_in_string_interps |
||||
- unnecessary_const |
||||
- unnecessary_getters_setters |
||||
- unnecessary_lambdas |
||||
- unnecessary_new |
||||
- unnecessary_null_aware_assignments |
||||
- unnecessary_null_in_if_null_operators |
||||
- unnecessary_overrides |
||||
- unnecessary_parenthesis |
||||
- unnecessary_statements |
||||
- unnecessary_this |
||||
- unrelated_type_equality_checks |
||||
- use_rethrow_when_possible |
||||
- valid_regexps |
||||
- void_checks |
@ -0,0 +1,5 @@
|
||||
library qiniu_sdk_base; |
||||
|
||||
export 'src/auth/auth.dart'; |
||||
export 'src/error/error.dart'; |
||||
export 'src/storage/storage.dart'; |
@ -0,0 +1,125 @@
|
||||
import 'dart:convert'; |
||||
import 'package:crypto/crypto.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import './put_policy.dart'; |
||||
|
||||
export 'put_policy.dart'; |
||||
|
||||
class TokenInfo { |
||||
final String accessKey; |
||||
final PutPolicy putPolicy; |
||||
const TokenInfo(this.accessKey, this.putPolicy); |
||||
} |
||||
|
||||
/// 提供用于鉴权的相关功能。 |
||||
/// |
||||
/// 更多信息请查看[官方文档-安全机制](https://developer.qiniu.com/kodo/manual/1644/security) |
||||
class Auth { |
||||
/// 鉴权所需的 [accessKey]。 |
||||
/// |
||||
/// 更多信息请查看[官方文档-密钥 AccessKey/SecretKey](https://developer.qiniu.com/kodo/manual/1644/security#aksk) |
||||
/// 使用须知请查看[官方文档-密钥安全使用须知](https://developer.qiniu.com/kodo/kb/1334/the-access-key-secret-key-encryption-key-safe-use-instructions) |
||||
final String accessKey; |
||||
|
||||
/// 鉴权所需的 [secretKey]。 |
||||
/// |
||||
/// 如何生成以及查看、使用等请参阅 [accessKey] 的说明 |
||||
final String secretKey; |
||||
|
||||
const Auth({ |
||||
@required this.accessKey, |
||||
@required this.secretKey, |
||||
}) : assert(accessKey != null), |
||||
assert(secretKey != null); |
||||
|
||||
/// 根据上传策略生成上传使用的 Token。 |
||||
/// |
||||
/// 具体的上传策略说明请参考 [PutPolicy] 模块 |
||||
String generateUploadToken({ |
||||
@required PutPolicy putPolicy, |
||||
}) { |
||||
assert(putPolicy != null); |
||||
|
||||
var data = jsonEncode(putPolicy); |
||||
var encodedPutPolicy = base64Url.encode(utf8.encode(data)); |
||||
var baseToken = generateAccessToken(bytes: utf8.encode(encodedPutPolicy)); |
||||
return '$baseToken:$encodedPutPolicy'; |
||||
} |
||||
|
||||
/// 生成针对私有空间资源的下载 Token。 |
||||
/// |
||||
/// [key] 为对象的名称 |
||||
/// [deadline] 有效时间,单位为秒,例如 1451491200 |
||||
/// [bucketDomain] 空间绑定的域名,例如 http://test.bucket.com |
||||
String generateDownloadToken({ |
||||
@required String key, |
||||
@required int deadline, |
||||
@required String bucketDomain, |
||||
}) { |
||||
assert(key != null); |
||||
assert(deadline != null); |
||||
assert(bucketDomain != null); |
||||
|
||||
var downloadURL = '$bucketDomain/$key?e=$deadline'; |
||||
return generateAccessToken(bytes: utf8.encode(downloadURL)); |
||||
} |
||||
|
||||
/// 根据数据签名,生成 Token(用于接口的访问鉴权)。 |
||||
/// |
||||
/// 访问七牛的接口需要对请求进行签名, 该方法提供 Token 签发服务 |
||||
String generateAccessToken({@required List<int> bytes}) { |
||||
assert(bytes != null); |
||||
|
||||
var hmacEncoder = Hmac(sha1, utf8.encode(secretKey)); |
||||
|
||||
var sign = hmacEncoder.convert(bytes); |
||||
var encodedSign = base64Url.encode(sign.bytes); |
||||
return '$accessKey:$encodedSign'; |
||||
} |
||||
|
||||
/// 解析 token 信息。 |
||||
/// |
||||
/// 从 Token 字符串中解析 [accessKey]、[PutPolicy] 信息 |
||||
static TokenInfo parseToken(String token) { |
||||
assert(token != null && token != ''); |
||||
|
||||
var segments = token.split(':'); |
||||
if (segments.length < 2) { |
||||
throw ArgumentError('invalid token'); |
||||
} |
||||
|
||||
PutPolicy putPolicy; |
||||
var accessKey = segments.first; |
||||
|
||||
/// 具体的 token 信息可以参考这里。 |
||||
/// [内部文档](https://github.com/qbox/product/blob/master/kodo/auths/UpToken.md#admin-uptoken-authorization) |
||||
if (segments.length >= 3) { |
||||
if (segments.last == '') { |
||||
throw ArgumentError('invalid token'); |
||||
} |
||||
|
||||
putPolicy = PutPolicy.fromJson(jsonDecode( |
||||
String.fromCharCodes( |
||||
base64Url.decode( |
||||
segments.last, |
||||
), |
||||
), |
||||
) as Map<String, dynamic>); |
||||
} |
||||
|
||||
return TokenInfo(accessKey, putPolicy); |
||||
} |
||||
|
||||
/// 解析 up token 信息。 |
||||
/// |
||||
/// 从 Token 字符串中解析 [accessKey]、[PutPolicy] 信息 |
||||
static TokenInfo parseUpToken(String token) { |
||||
assert(token != null && token != ''); |
||||
final tokenInfo = parseToken(token); |
||||
if (tokenInfo.putPolicy == null) { |
||||
throw ArgumentError('invalid up token'); |
||||
} |
||||
|
||||
return tokenInfo; |
||||
} |
||||
} |
@ -0,0 +1,209 @@
|
||||
import 'package:meta/meta.dart'; |
||||
|
||||
/// 上传策略 |
||||
/// |
||||
/// 更多信息请查看[官方文档-上传策略](https://developer.qiniu.com/kodo/manual/1206/put-policy) |
||||
class PutPolicy { |
||||
/// 指定上传的目标资源空间 Bucket 和资源键 Key(最大为 750 字节)。 |
||||
/// |
||||
/// 有三种格式: |
||||
/// <Bucket> 表示允许用户上传文件到指定的 Bucket,在这种格式下文件只能新增。 |
||||
/// <Bucket>:<Key> 表示只允许用户上传指定 Key 的文件。在这种格式下文件默认允许修改。 |
||||
/// <Bucket>:<KeyPrefix> 表示只允许用户上传指定以 KeyPrefix 为前缀的文件。 |
||||
/// 具体信息一定请查看上述的上传策略文档! |
||||
final String scope; |
||||
|
||||
/// 获取 Bucket。 |
||||
/// |
||||
/// 从 [scope] 中获取 Bucket。 |
||||
String getBucket() { |
||||
return scope.split(':').first; |
||||
} |
||||
|
||||
/// 若为 1,表示允许用户上传以 [scope] 的 KeyPrefix 为前缀的文件。 |
||||
final int isPrefixalScope; |
||||
|
||||
/// 上传凭证有效截止时间。 |
||||
/// |
||||
/// Unix 时间戳,单位为秒, |
||||
/// 该截止时间为上传完成后,在七牛空间生成文件的校验时间, 而非上传的开始时间, |
||||
/// 一般建议设置为上传开始时间 + 3600s。 |
||||
final int deadline; |
||||
|
||||
/// 限制为新增文件。 |
||||
/// |
||||
/// 如果设置为非 0 值,则无论 [scope] 设置为什么形式,仅能以新增模式上传文件。 |
||||
final int insertOnly; |
||||
|
||||
/// 唯一属主标识。 |
||||
/// |
||||
/// 特殊场景下非常有用,例如根据 App-Client 标识给图片或视频打水印。 |
||||
final String endUser; |
||||
|
||||
/// Web 端文件上传成功后,浏览器执行 303 跳转的 URL。 |
||||
/// |
||||
/// 文件上传成功后会跳转到 <[returnUrl]>?upload_ret=<QueryString> |
||||
/// 其中 <QueryString> 包含 [returnBody] 内容。 |
||||
/// 如不设置 [returnUrl],则直接将 [returnBody] 的内容返回给客户端。 |
||||
final String returnUrl; |
||||
|
||||
/// [returnBody] 声明服务端的响应格式。 |
||||
/// |
||||
/// 可以使用 <魔法变量> 和 <自定义变量>,必须是合法的 JSON 格式, |
||||
/// 关于 <魔法变量> 请参阅:[官方文档-魔法变量](https://developer.qiniu.com/kodo/manual/1235/vars#magicvar) |
||||
/// 关于 <自定义变量> 请参阅:[官方文档-自定义变量](https://developer.qiniu.com/kodo/manual/1235/vars#xvar) |
||||
final String returnBody; |
||||
|
||||
/// 上传成功后,七牛云向业务服务器发送 POST 请求的 URL。 |
||||
final String callbackUrl; |
||||
|
||||
/// 上传成功后,七牛云向业务服务器发送回调通知时的 Host 值。 |
||||
/// |
||||
/// 与 [callbackUrl] 配合使用,仅当设置了 [callbackUrl] 时才有效。 |
||||
final String callbackHost; |
||||
|
||||
/// 上传成功后发起的回调请求。 |
||||
/// |
||||
/// 七牛云向业务服务器发送 Content-Type: application/x-www-form-urlencoded 的 POST 请求, |
||||
/// 例如:{"key":"$(key)","hash":"$(etag)","w":"$(imageInfo.width)","h":"$(imageInfo.height)"}, |
||||
/// 可以使用 <魔法变量> 和 <自定义变量>。 |
||||
final String callbackBody; |
||||
|
||||
/// 上传成功后发起的回调请求的 Content-Type。 |
||||
/// |
||||
/// 默认为 application/x-www-form-urlencoded,也可设置为 application/json。 |
||||
final String callbackBodyType; |
||||
|
||||
/// 资源上传成功后触发执行的预转持久化处理指令列表。 |
||||
/// |
||||
/// [fileType] = 2(上传归档存储文件)时,不支持使用该参数, |
||||
/// 每个指令是一个 API 规格字符串,多个指令用 ; 分隔, |
||||
/// 可以使用 <魔法变量> 和 <自定义变量>, |
||||
/// 改字段的具体使用信息可以查看:[官方文档-#persistentOps](https://developer.qiniu.com/kodo/manual/1206/put-policy#persistentOps) |
||||
final String persistentOps; |
||||
|
||||
/// 接收持久化处理结果通知的 URL。 |
||||
/// |
||||
/// 必须是公网上可以正常进行 POST 请求并能响应 HTTP/1.1 200 OK 的有效 URL, |
||||
/// 该 URL 获取的内容和持久化处理状态查询的处理结果一致, |
||||
/// 发送 body 格式是 Content-Type 为 application/json 的 POST 请求, |
||||
/// 需要按照读取流的形式读取请求的 body 才能获取。 |
||||
final String persistentNotifyUrl; |
||||
|
||||
/// 转码队列名。 |
||||
/// |
||||
/// 资源上传成功后,触发转码时指定独立的队列进行转码, |
||||
/// 为空则表示使用公用队列,处理速度比较慢。建议使用专用队列。 |
||||
final String persistentPipeline; |
||||
|
||||
/// [saveKey] 的优先级设置。 |
||||
/// |
||||
/// 该设置为 true 时,[saveKey] 不能为空,会忽略客户端指定的 Key,强制使用 [saveKey] 进行文件命名。 |
||||
/// 参数不设置时,默认值为 false。 |
||||
final String forceSaveKey; |
||||
|
||||
/// 自定义资源名。 |
||||
/// |
||||
/// 支持<魔法变量>和<自定义变量>, [forceSaveKey] 为 false 时, |
||||
/// 这个字段仅当用户上传的时候没有主动指定 key 时起作用, |
||||
/// [forceSaveKey] 为 true 时,将强制按这个字段的格式命名。 |
||||
final String saveKey; |
||||
|
||||
/// 限定上传文件大小最小值,单位 Byte。 |
||||
final int fsizeMin; |
||||
|
||||
/// 限定上传文件大小最大值,单位 Byte。 |
||||
/// |
||||
/// 超过限制上传文件大小的最大值会被判为上传失败,返回 413 状态码。 |
||||
final int fsizeLimit; |
||||
|
||||
/// 开启 MimeType 侦测功能。 |
||||
final int detectMime; |
||||
|
||||
/// 限定用户上传的文件类型。 |
||||
final String mimeLimit; |
||||
|
||||
/// 文件存储类型 |
||||
/// |
||||
/// 0 为标准存储(默认), |
||||
/// 1 为低频存储, |
||||
/// 2 为归档存储。 |
||||
final int fileType; |
||||
|
||||
const PutPolicy({ |
||||
@required this.scope, |
||||
@required this.deadline, |
||||
this.isPrefixalScope, |
||||
this.insertOnly, |
||||
this.endUser, |
||||
this.returnUrl, |
||||
this.returnBody, |
||||
this.callbackUrl, |
||||
this.callbackHost, |
||||
this.callbackBody, |
||||
this.callbackBodyType, |
||||
this.persistentOps, |
||||
this.persistentNotifyUrl, |
||||
this.persistentPipeline, |
||||
this.forceSaveKey, |
||||
this.saveKey, |
||||
this.fsizeMin, |
||||
this.fsizeLimit, |
||||
this.detectMime, |
||||
this.mimeLimit, |
||||
this.fileType, |
||||
}) : assert(scope != null), |
||||
assert(deadline != null); |
||||
|
||||
Map<String, dynamic> toJson() { |
||||
return <String, dynamic>{ |
||||
'scope': scope, |
||||
'isPrefixalScope': isPrefixalScope, |
||||
'deadline': deadline, |
||||
'insertOnly': insertOnly, |
||||
'endUser': endUser, |
||||
'returnUrl': returnUrl, |
||||
'returnBody': returnBody, |
||||
'callbackUrl': callbackUrl, |
||||
'callbackHost': callbackHost, |
||||
'callbackBody': callbackBody, |
||||
'callbackBodyType': callbackBodyType, |
||||
'persistentOps': persistentOps, |
||||
'persistentNotifyUrl': persistentNotifyUrl, |
||||
'persistentPipeline': persistentPipeline, |
||||
'forceSaveKey': forceSaveKey, |
||||
'saveKey': saveKey, |
||||
'fsizeMin': fsizeMin, |
||||
'fsizeLimit': fsizeLimit, |
||||
'detectMime': detectMime, |
||||
'mimeLimit': mimeLimit, |
||||
'fileType': fileType, |
||||
}..removeWhere((key, dynamic value) => value == null); |
||||
} |
||||
|
||||
factory PutPolicy.fromJson(Map<String, dynamic> json) { |
||||
return PutPolicy( |
||||
scope: json['scope'] as String, |
||||
deadline: json['deadline'] as int, |
||||
isPrefixalScope: json['isPrefixalScope'] as int, |
||||
insertOnly: json['insertOnly'] as int, |
||||
endUser: json['endUser'] as String, |
||||
returnUrl: json['returnUrl'] as String, |
||||
returnBody: json['returnBody'] as String, |
||||
callbackUrl: json['callbackUrl'] as String, |
||||
callbackHost: json['callbackHost'] as String, |
||||
callbackBody: json['callbackBody'] as String, |
||||
callbackBodyType: json['callbackBodyType'] as String, |
||||
persistentOps: json['persistentOps'] as String, |
||||
persistentNotifyUrl: json['persistentNotifyUrl'] as String, |
||||
persistentPipeline: json['persistentPipeline'] as String, |
||||
forceSaveKey: json['forceSaveKey'] as String, |
||||
saveKey: json['saveKey'] as String, |
||||
fsizeMin: json['fsizeMin'] as int, |
||||
fsizeLimit: json['fsizeLimit'] as int, |
||||
detectMime: json['detectMime'] as int, |
||||
mimeLimit: json['mimeLimit'] as String, |
||||
fileType: json['fileType'] as int, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,12 @@
|
||||
class QiniuError extends Error { |
||||
final Error rawError; |
||||
|
||||
final String _message; |
||||
|
||||
String get message => _message ?? rawError?.toString() ?? ''; |
||||
|
||||
@override |
||||
StackTrace get stackTrace => rawError?.stackTrace ?? super.stackTrace; |
||||
|
||||
QiniuError({this.rawError, String message}) : _message = message; |
||||
} |
@ -0,0 +1,39 @@
|
||||
part of 'config.dart'; |
||||
|
||||
abstract class CacheProvider { |
||||
/// 设置一对数据 |
||||
Future setItem(String key, String item); |
||||
|
||||
/// 根据 key 获取缓存 |
||||
Future<String> getItem(String key); |
||||
|
||||
/// 删除指定 key 的缓存 |
||||
Future removeItem(String key); |
||||
|
||||
/// 清除所有 |
||||
Future clear(); |
||||
} |
||||
|
||||
class DefaultCacheProvider extends CacheProvider { |
||||
Map<String, String> value = {}; |
||||
|
||||
@override |
||||
Future clear() async { |
||||
value.clear(); |
||||
} |
||||
|
||||
@override |
||||
Future<String> getItem(String key) async { |
||||
return value[key]; |
||||
} |
||||
|
||||
@override |
||||
Future removeItem(String key) async { |
||||
value.remove(key); |
||||
} |
||||
|
||||
@override |
||||
Future setItem(String key, String item) async { |
||||
value[key] = item; |
||||
} |
||||
} |
@ -0,0 +1,28 @@
|
||||
import 'package:dio/adapter.dart'; |
||||
import 'package:dio/dio.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:qiniu_sdk_base/src/storage/error/error.dart'; |
||||
|
||||
part 'protocol.dart'; |
||||
part 'host.dart'; |
||||
part 'cache.dart'; |
||||
|
||||
class Config { |
||||
final HostProvider hostProvider; |
||||
final CacheProvider cacheProvider; |
||||
final HttpClientAdapter httpClientAdapter; |
||||
|
||||
/// 重试次数 |
||||
/// |
||||
/// 各种网络请求失败的重试次数 |
||||
final int retryLimit; |
||||
|
||||
Config({ |
||||
HostProvider hostProvider, |
||||
CacheProvider cacheProvider, |
||||
HttpClientAdapter httpClientAdapter, |
||||
this.retryLimit = 3, |
||||
}) : hostProvider = hostProvider ?? DefaultHostProvider(), |
||||
cacheProvider = cacheProvider ?? DefaultCacheProvider(), |
||||
httpClientAdapter = httpClientAdapter ?? DefaultHttpClientAdapter(); |
||||
} |
@ -0,0 +1,124 @@
|
||||
part of 'config.dart'; |
||||
|
||||
abstract class HostProvider { |
||||
Future<String> getUpHost({ |
||||
@required String accessKey, |
||||
@required String bucket, |
||||
}); |
||||
|
||||
bool isFrozen(String host); |
||||
|
||||
void freezeHost(String host); |
||||
} |
||||
|
||||
class DefaultHostProvider extends HostProvider { |
||||
final protocol = Protocol.Https.value; |
||||
|
||||
final _http = Dio(); |
||||
// 缓存的上传区域 |
||||
final _stashedUpDomains = <_Domain>[]; |
||||
// accessKey:bucket 用此 key 判断是否 up host 需要走缓存 |
||||
String _cacheKey; |
||||
// 冻结的上传区域 |
||||
final List<_Domain> _frozenUpDomains = []; |
||||
|
||||
@override |
||||
Future<String> getUpHost({ |
||||
@required String accessKey, |
||||
@required String bucket, |
||||
}) async { |
||||
// 解冻需要被解冻的 host |
||||
_frozenUpDomains.removeWhere((domain) => !domain.isFrozen()); |
||||
|
||||
var _upDomains = <_Domain>[]; |
||||
if ('$accessKey:$bucket' == _cacheKey && _stashedUpDomains.isNotEmpty) { |
||||
_upDomains.addAll(_stashedUpDomains); |
||||
} else { |
||||
final url = |
||||
'$protocol://api.qiniu.com/v4/query?ak=$accessKey&bucket=$bucket'; |
||||
|
||||
final res = await _http.get<Map>(url); |
||||
|
||||
final hosts = res.data['hosts'] |
||||
.map((dynamic json) => _Host.fromJson(json as Map)) |
||||
.cast<_Host>() |
||||
.toList() as List<_Host>; |
||||
|
||||
for (var host in hosts) { |
||||
final domainList = host.up['domains'].cast<String>() as List<String>; |
||||
final domains = domainList.map((domain) => _Domain(domain)); |
||||
_upDomains.addAll(domains); |
||||
} |
||||
|
||||
_cacheKey = '$accessKey:$bucket'; |
||||
_stashedUpDomains.addAll(_upDomains); |
||||
} |
||||
|
||||
// 每次都从头遍历一遍,最合适的 host 总是会排在最前面 |
||||
for (var index = 0; index < _upDomains.length; index++) { |
||||
final availableDomain = _upDomains.elementAt(index); |
||||
// 检查看起来可用的 host 是否之前被冻结过 |
||||
final frozen = isFrozen(protocol + '://' + availableDomain.value); |
||||
|
||||
if (!frozen) { |
||||
return protocol + '://' + availableDomain.value; |
||||
} |
||||
} |
||||
// 全部被冻结,几乎不存在的情况 |
||||
throw StorageError( |
||||
type: StorageErrorType.NO_AVAILABLE_HOST, |
||||
message: '没有可用的上传域名', |
||||
); |
||||
} |
||||
|
||||
@override |
||||
bool isFrozen(String host) { |
||||
final uri = Uri.parse(host); |
||||
final frozenDomain = _frozenUpDomains.firstWhere( |
||||
(domain) => domain.isFrozen() && domain.value == uri.host, |
||||
orElse: () => null); |
||||
return frozenDomain != null; |
||||
} |
||||
|
||||
@override |
||||
void freezeHost(String host) { |
||||
// http://example.org |
||||
// scheme: http |
||||
// host: example.org |
||||
final uri = Uri.parse(host); |
||||
_frozenUpDomains.add(_Domain(uri.host)..freeze()); |
||||
} |
||||
} |
||||
|
||||
class _Host { |
||||
String region; |
||||
int ttl; |
||||
// domains: [] |
||||
Map<String, dynamic> up; |
||||
|
||||
_Host({this.region, this.ttl, this.up}); |
||||
|
||||
factory _Host.fromJson(Map json) { |
||||
return _Host( |
||||
region: json['region'] as String, |
||||
ttl: json['ttl'] as int, |
||||
up: json['up'] as Map<String, dynamic>, |
||||
); |
||||
} |
||||
} |
||||
|
||||
class _Domain { |
||||
int frozenTime = 0; |
||||
final _lockTime = 1000 * 60 * 10; |
||||
|
||||
bool isFrozen() { |
||||
return frozenTime + _lockTime > DateTime.now().millisecondsSinceEpoch; |
||||
} |
||||
|
||||
void freeze() { |
||||
frozenTime = DateTime.now().millisecondsSinceEpoch; |
||||
} |
||||
|
||||
String value; |
||||
_Domain(this.value); |
||||
} |
@ -0,0 +1,11 @@
|
||||
part of 'config.dart'; |
||||
|
||||
enum Protocol { Http, Https } |
||||
|
||||
extension ProtocolExt on Protocol { |
||||
String get value { |
||||
if (this == Protocol.Http) return 'http'; |
||||
if (this == Protocol.Https) return 'https'; |
||||
return 'https'; |
||||
} |
||||
} |
@ -0,0 +1,75 @@
|
||||
import 'package:dio/dio.dart'; |
||||
import 'package:qiniu_sdk_base/src/error/error.dart'; |
||||
|
||||
enum StorageErrorType { |
||||
/// 连接超时 |
||||
CONNECT_TIMEOUT, |
||||
|
||||
/// 发送超时 |
||||
SEND_TIMEOUT, |
||||
|
||||
/// 接收超时 |
||||
RECEIVE_TIMEOUT, |
||||
|
||||
/// 服务端响应了但是状态码是 400 以上 |
||||
RESPONSE, |
||||
|
||||
/// 请求被取消 |
||||
CANCEL, |
||||
|
||||
/// 没有可用的服务器 |
||||
NO_AVAILABLE_HOST, |
||||
|
||||
/// 已在处理队列中 |
||||
IN_PROGRESS, |
||||
|
||||
/// 未知或者不能处理的错误 |
||||
UNKNOWN, |
||||
} |
||||
|
||||
class StorageError extends QiniuError { |
||||
/// [type] 不是 [StorageErrorType.RESPONSE] 的时候为 null |
||||
final int code; |
||||
final StorageErrorType type; |
||||
|
||||
StorageError({this.type, this.code, Error rawError, String message}) |
||||
: super(rawError: rawError, message: message); |
||||
|
||||
factory StorageError.fromError(Error error) { |
||||
return StorageError(type: StorageErrorType.UNKNOWN, rawError: error); |
||||
} |
||||
|
||||
factory StorageError.fromDioError(DioError error) { |
||||
return StorageError( |
||||
type: _mapDioErrorType(error.type), |
||||
code: error.response?.statusCode, |
||||
message: error.response?.data.toString(), |
||||
rawError: error.error is Error ? (error.error as Error) : null, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
String toString() { |
||||
var msg = 'StorageError [$type, $code]: $message'; |
||||
msg += '\n$stackTrace'; |
||||
return msg; |
||||
} |
||||
} |
||||
|
||||
StorageErrorType _mapDioErrorType(DioErrorType type) { |
||||
switch (type) { |
||||
case DioErrorType.CONNECT_TIMEOUT: |
||||
return StorageErrorType.CONNECT_TIMEOUT; |
||||
case DioErrorType.SEND_TIMEOUT: |
||||
return StorageErrorType.SEND_TIMEOUT; |
||||
case DioErrorType.RECEIVE_TIMEOUT: |
||||
return StorageErrorType.RECEIVE_TIMEOUT; |
||||
case DioErrorType.RESPONSE: |
||||
return StorageErrorType.RESPONSE; |
||||
case DioErrorType.CANCEL: |
||||
return StorageErrorType.CANCEL; |
||||
case DioErrorType.DEFAULT: |
||||
default: |
||||
return StorageErrorType.UNKNOWN; |
||||
} |
||||
} |
@ -0,0 +1,20 @@
|
||||
part of 'put_parts_task.dart'; |
||||
|
||||
/// 分片上传用到的缓存 mixin |
||||
/// |
||||
/// 分片上传的初始化文件、上传分片都应该以此实现缓存控制策略 |
||||
mixin CacheMixin<T> on RequestTask<T> { |
||||
String get _cacheKey; |
||||
|
||||
Future clearCache() async { |
||||
await config.cacheProvider.removeItem(_cacheKey); |
||||
} |
||||
|
||||
Future setCache(String data) async { |
||||
await config.cacheProvider.setItem(_cacheKey, data); |
||||
} |
||||
|
||||
Future<String> getCache() async { |
||||
return await config.cacheProvider.getItem(_cacheKey); |
||||
} |
||||
} |
@ -0,0 +1,51 @@
|
||||
part of 'put_parts_task.dart'; |
||||
|
||||
/// 创建文件,把切片信息合成为一个文件 |
||||
class CompletePartsTask extends RequestTask<PutResponse> { |
||||
final String token; |
||||
final String uploadId; |
||||
final List<Part> parts; |
||||
final String key; |
||||
|
||||
TokenInfo _tokenInfo; |
||||
|
||||
CompletePartsTask({ |
||||
@required this.token, |
||||
@required this.uploadId, |
||||
@required this.parts, |
||||
this.key, |
||||
PutController controller, |
||||
}) : super(controller: controller); |
||||
|
||||
@override |
||||
void preStart() { |
||||
_tokenInfo = Auth.parseUpToken(token); |
||||
super.preStart(); |
||||
} |
||||
|
||||
@override |
||||
Future<PutResponse> createTask() async { |
||||
final bucket = _tokenInfo.putPolicy.getBucket(); |
||||
|
||||
final host = await config.hostProvider.getUpHost( |
||||
bucket: bucket, |
||||
accessKey: _tokenInfo.accessKey, |
||||
); |
||||
final headers = <String, dynamic>{'Authorization': 'UpToken $token'}; |
||||
final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~'; |
||||
final paramUrl = |
||||
'$host/buckets/$bucket/objects/$encodedKey/uploads/$uploadId'; |
||||
|
||||
final response = await client.post<Map<String, dynamic>>( |
||||
paramUrl, |
||||
data: { |
||||
'parts': parts |
||||
..sort((a, b) => a.partNumber - b.partNumber) |
||||
..map((part) => part.toJson()).toList() |
||||
}, |
||||
options: Options(headers: headers), |
||||
); |
||||
|
||||
return PutResponse.fromJson(response.data); |
||||
} |
||||
} |
@ -0,0 +1,92 @@
|
||||
part of 'put_parts_task.dart'; |
||||
|
||||
/// initParts 的返回体 |
||||
class InitParts { |
||||
final int expireAt; |
||||
final String uploadId; |
||||
|
||||
InitParts({ |
||||
@required this.expireAt, |
||||
@required this.uploadId, |
||||
}); |
||||
|
||||
factory InitParts.fromJson(Map json) { |
||||
return InitParts( |
||||
uploadId: json['uploadId'] as String, |
||||
expireAt: json['expireAt'] as int, |
||||
); |
||||
} |
||||
|
||||
Map<String, dynamic> toJson() { |
||||
return <String, dynamic>{ |
||||
'uploadId': uploadId, |
||||
'expireAt': expireAt, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
/// 初始化一个分片上传任务,为 [UploadPartsTask] 提供 uploadId |
||||
class InitPartsTask extends RequestTask<InitParts> with CacheMixin<InitParts> { |
||||
final File file; |
||||
final String token; |
||||
final String key; |
||||
|
||||
@override |
||||
String _cacheKey; |
||||
TokenInfo _tokenInfo; |
||||
|
||||
InitPartsTask({ |
||||
@required this.file, |
||||
@required this.token, |
||||
this.key, |
||||
PutController controller, |
||||
}) : super(controller: controller); |
||||
|
||||
static String getCacheKey(String path, int length, String key) { |
||||
return 'qiniu_dart_sdk_init_parts_task_${path}_key_${key}_size_$length'; |
||||
} |
||||
|
||||
@override |
||||
void preStart() { |
||||
_tokenInfo = Auth.parseUpToken(token); |
||||
_cacheKey = InitPartsTask.getCacheKey(file.path, file.lengthSync(), key); |
||||
super.preStart(); |
||||
} |
||||
|
||||
@override |
||||
Future<InitParts> createTask() async { |
||||
final headers = {'Authorization': 'UpToken $token'}; |
||||
|
||||
final initPartsCache = await getCache(); |
||||
if (initPartsCache != null) { |
||||
return InitParts.fromJson( |
||||
json.decode(initPartsCache) as Map<String, dynamic>); |
||||
} |
||||
|
||||
final bucket = _tokenInfo.putPolicy.getBucket(); |
||||
|
||||
final host = await config.hostProvider.getUpHost( |
||||
bucket: bucket, |
||||
accessKey: _tokenInfo.accessKey, |
||||
); |
||||
|
||||
final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~'; |
||||
final paramUrl = '$host/buckets/$bucket/objects/$encodedKey/uploads'; |
||||
|
||||
final response = await client.post<Map<String, dynamic>>( |
||||
paramUrl, |
||||
|
||||
/// 这里 data 不传,dio 不会触发 cancel 事件 |
||||
data: <String, dynamic>{}, |
||||
options: Options(headers: headers), |
||||
); |
||||
|
||||
return InitParts.fromJson(response.data); |
||||
} |
||||
|
||||
@override |
||||
void postReceive(data) async { |
||||
await setCache(json.encode(data.toJson())); |
||||
super.postReceive(data); |
||||
} |
||||
} |
@ -0,0 +1,26 @@
|
||||
part of 'put_parts_task.dart'; |
||||
|
||||
/// 切片信息 |
||||
class Part { |
||||
final String etag; |
||||
final int partNumber; |
||||
|
||||
Part({ |
||||
@required this.etag, |
||||
@required this.partNumber, |
||||
}); |
||||
|
||||
factory Part.fromJson(Map<String, dynamic> json) { |
||||
return Part( |
||||
etag: json['etag'] as String, |
||||
partNumber: json['partNumber'] as int, |
||||
); |
||||
} |
||||
|
||||
Map<String, dynamic> toJson() { |
||||
return <String, dynamic>{ |
||||
'etag': etag, |
||||
'partNumber': partNumber, |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,25 @@
|
||||
import '../put_controller.dart'; |
||||
|
||||
class PutByPartOptions { |
||||
/// 资源名 |
||||
/// 如果不传则后端自动生成 |
||||
final String key; |
||||
|
||||
/// 切片大小,单位 MB |
||||
/// |
||||
/// 超出 [partSize] 的文件大小会把每片按照 [partSize] 的大小切片并上传 |
||||
/// 默认 4MB,最小不得小于 1MB,最大不得大于 1024 MB |
||||
final int partSize; |
||||
|
||||
final int maxPartsRequestNumber; |
||||
|
||||
/// 控制器 |
||||
final PutController controller; |
||||
|
||||
const PutByPartOptions({ |
||||
this.key, |
||||
this.partSize = 4, |
||||
this.maxPartsRequestNumber = 5, |
||||
this.controller, |
||||
}); |
||||
} |
@ -0,0 +1,187 @@
|
||||
import 'dart:async'; |
||||
import 'dart:convert'; |
||||
import 'dart:io'; |
||||
import 'dart:math'; |
||||
|
||||
import 'package:dio/dio.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; |
||||
|
||||
part 'cache_mixin.dart'; |
||||
part 'complete_parts_task.dart'; |
||||
part 'init_parts_task.dart'; |
||||
part 'part.dart'; |
||||
part 'upload_part_task.dart'; |
||||
part 'upload_parts_task.dart'; |
||||
|
||||
/// 分片上传任务 |
||||
class PutByPartTask extends RequestTask<PutResponse> { |
||||
final File file; |
||||
final String token; |
||||
|
||||
final int partSize; |
||||
final int maxPartsRequestNumber; |
||||
|
||||
final String key; |
||||
|
||||
/// 设置为 0,避免子任务重试失败后 [PutByPartTask] 继续重试 |
||||
@override |
||||
int get retryLimit => 0; |
||||
|
||||
PutByPartTask({ |
||||
@required this.file, |
||||
@required this.token, |
||||
@required this.partSize, |
||||
@required this.maxPartsRequestNumber, |
||||
this.key, |
||||
PutController controller, |
||||
}) : assert(file != null), |
||||
assert(token != null), |
||||
assert(partSize != null), |
||||
assert(maxPartsRequestNumber != null), |
||||
assert(() { |
||||
if (partSize < 1 || partSize > 1024) { |
||||
throw RangeError.range(partSize, 1, 1024, 'partSize', |
||||
'partSize must be greater than 1 and less than 1024'); |
||||
} |
||||
return true; |
||||
}()), |
||||
super(controller: controller); |
||||
|
||||
RequestTaskController _currentWorkingTaskController; |
||||
|
||||
@override |
||||
void preStart() { |
||||
super.preStart(); |
||||
|
||||
// 处理相同任务 |
||||
final sameTaskExsist = manager.getTasks().firstWhere( |
||||
(element) => element is PutByPartTask && isEquals(element), |
||||
orElse: () => null, |
||||
); |
||||
|
||||
if (sameTaskExsist != null) { |
||||
throw StorageError( |
||||
type: StorageErrorType.IN_PROGRESS, |
||||
message: '$file 已在上传队列中', |
||||
); |
||||
} |
||||
|
||||
// controller 被取消后取消当前运行的子任务 |
||||
controller?.cancelToken?.whenCancel?.then((_) { |
||||
_currentWorkingTaskController?.cancel(); |
||||
}); |
||||
} |
||||
|
||||
@override |
||||
void postReceive(PutResponse data) { |
||||
_currentWorkingTaskController = null; |
||||
super.postReceive(data); |
||||
} |
||||
|
||||
@override |
||||
Future<PutResponse> createTask() async { |
||||
controller?.notifyStatusListeners(StorageStatus.Request); |
||||
|
||||
final initPartsTask = _createInitParts(); |
||||
final initParts = await initPartsTask.future; |
||||
|
||||
// 初始化任务完成后也告诉外部一个进度 |
||||
controller?.notifyProgressListeners(0.002); |
||||
|
||||
final uploadParts = _createUploadParts(initParts.uploadId); |
||||
|
||||
PutResponse putResponse; |
||||
try { |
||||
final parts = await uploadParts.future; |
||||
putResponse = |
||||
await _createCompleteParts(initParts.uploadId, parts).future; |
||||
} catch (error) { |
||||
// 拿不到 initPartsTask 和 uploadParts 的引用,所以不放到 postError 去 |
||||
if (error is StorageError) { |
||||
/// 满足以下两种情况清理缓存: |
||||
/// 1、如果服务端文件被删除了,清除本地缓存 |
||||
/// 2、如果 uploadId 等参数不对原因会导致 400 |
||||
if (error.code == 612 || error.code == 400) { |
||||
await initPartsTask.clearCache(); |
||||
await uploadParts.clearCache(); |
||||
} |
||||
|
||||
/// 如果服务端文件被删除了,重新上传 |
||||
if (error.code == 612) { |
||||
controller?.notifyStatusListeners(StorageStatus.Retry); |
||||
return createTask(); |
||||
} |
||||
} |
||||
|
||||
rethrow; |
||||
} |
||||
|
||||
/// 上传完成,清除缓存 |
||||
await initPartsTask.clearCache(); |
||||
await uploadParts.clearCache(); |
||||
|
||||
return putResponse; |
||||
} |
||||
|
||||
bool isEquals(PutByPartTask target) { |
||||
return target.file.path == file.path && |
||||
target.key == key && |
||||
target.file.lengthSync() == file.lengthSync(); |
||||
} |
||||
|
||||
/// 初始化上传信息,分片上传的第一步 |
||||
InitPartsTask _createInitParts() { |
||||
final _controller = PutController(); |
||||
|
||||
final task = InitPartsTask( |
||||
file: file, |
||||
token: token, |
||||
key: key, |
||||
controller: _controller, |
||||
); |
||||
|
||||
manager.addTask(task); |
||||
_currentWorkingTaskController = _controller; |
||||
return task; |
||||
} |
||||
|
||||
UploadPartsTask _createUploadParts(String uploadId) { |
||||
final _controller = PutController(); |
||||
|
||||
final task = UploadPartsTask( |
||||
file: file, |
||||
token: token, |
||||
partSize: partSize, |
||||
uploadId: uploadId, |
||||
maxPartsRequestNumber: maxPartsRequestNumber, |
||||
key: key, |
||||
controller: _controller, |
||||
); |
||||
|
||||
_controller.addSendProgressListener(onSendProgress); |
||||
|
||||
manager.addTask(task); |
||||
_currentWorkingTaskController = _controller; |
||||
return task; |
||||
} |
||||
|
||||
/// 创建文件,分片上传的最后一步 |
||||
CompletePartsTask _createCompleteParts( |
||||
String uploadId, |
||||
List<Part> parts, |
||||
) { |
||||
final _controller = PutController(); |
||||
final task = CompletePartsTask( |
||||
token: token, |
||||
uploadId: uploadId, |
||||
parts: parts, |
||||
key: key, |
||||
controller: _controller, |
||||
); |
||||
|
||||
manager.addTask(task); |
||||
_currentWorkingTaskController = _controller; |
||||
return task; |
||||
} |
||||
} |
@ -0,0 +1,112 @@
|
||||
part of 'put_parts_task.dart'; |
||||
|
||||
// 上传一个 part 的任务 |
||||
class UploadPartTask extends RequestTask<UploadPart> { |
||||
final String token; |
||||
final String uploadId; |
||||
final RandomAccessFile raf; |
||||
final int partSize; |
||||
|
||||
// 如果 data 是 Stream 的话,Dio 需要判断 content-length 才会调用 onSendProgress |
||||
// https://github.com/flutterchina/dio/blob/21136168ab39a7536835c7a59ce0465bb05feed4/dio/lib/src/dio.dart#L1000 |
||||
final int byteLength; |
||||
|
||||
final int partNumber; |
||||
|
||||
final String key; |
||||
|
||||
TokenInfo _tokenInfo; |
||||
|
||||
UploadPartTask({ |
||||
@required this.token, |
||||
@required this.raf, |
||||
@required this.uploadId, |
||||
@required this.byteLength, |
||||
@required this.partNumber, |
||||
@required this.partSize, |
||||
this.key, |
||||
PutController controller, |
||||
}) : super(controller: controller); |
||||
|
||||
@override |
||||
void preStart() { |
||||
_tokenInfo = Auth.parseUpToken(token); |
||||
super.preStart(); |
||||
} |
||||
|
||||
@override |
||||
void postReceive(data) { |
||||
controller?.notifyProgressListeners(1); |
||||
super.postReceive(data); |
||||
} |
||||
|
||||
@override |
||||
Future<UploadPart> createTask() async { |
||||
final headers = <String, dynamic>{ |
||||
'Authorization': 'UpToken $token', |
||||
Headers.contentLengthHeader: byteLength, |
||||
}; |
||||
|
||||
final bucket = _tokenInfo.putPolicy.getBucket(); |
||||
|
||||
final host = await config.hostProvider.getUpHost( |
||||
bucket: bucket, |
||||
accessKey: _tokenInfo.accessKey, |
||||
); |
||||
|
||||
final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~'; |
||||
final paramUrl = 'buckets/$bucket/objects/$encodedKey'; |
||||
|
||||
final response = await client.put<Map<String, dynamic>>( |
||||
'$host/$paramUrl/uploads/$uploadId/$partNumber', |
||||
data: Stream.fromIterable([_readFileByPartNumber(partNumber)]), |
||||
// 在 data 是 stream 的场景下, interceptor 传入 cancelToken 这里不传会有 bug |
||||
cancelToken: controller.cancelToken, |
||||
options: Options(headers: headers), |
||||
); |
||||
|
||||
return UploadPart.fromJson(response.data); |
||||
} |
||||
|
||||
// 分片上传是手动从 File 拿一段数据大概 4m(直穿是直接从 File 里面读取) |
||||
// 如果文件是 21m,假设切片是 4 * 5 |
||||
// 外部进度的话会导致一下长到 90% 多,然后变成 100% |
||||
// 解决方法是覆盖父类的 onSendProgress,让 onSendProgress 不处理 Progress 的进度 |
||||
// 改为发送成功后通知(见 postReceive) |
||||
@override |
||||
void onSendProgress(double percent) { |
||||
controller?.notifySendProgressListeners(percent); |
||||
} |
||||
|
||||
// 根据 partNumber 获取要上传的文件片段 |
||||
List<int> _readFileByPartNumber(int partNumber) { |
||||
final startOffset = (partNumber - 1) * partSize * 1024 * 1024; |
||||
raf.setPositionSync(startOffset); |
||||
return raf.readSync(byteLength); |
||||
} |
||||
} |
||||
|
||||
// uploadPart 的返回体 |
||||
class UploadPart { |
||||
final String md5; |
||||
final String etag; |
||||
|
||||
UploadPart({ |
||||
@required this.md5, |
||||
@required this.etag, |
||||
}); |
||||
|
||||
factory UploadPart.fromJson(Map json) { |
||||
return UploadPart( |
||||
md5: json['md5'] as String, |
||||
etag: json['etag'] as String, |
||||
); |
||||
} |
||||
|
||||
Map<String, dynamic> toJson() { |
||||
return <String, dynamic>{ |
||||
'etag': etag, |
||||
'md5': md5, |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,260 @@
|
||||
part of 'put_parts_task.dart'; |
||||
|
||||
// 批处理上传 parts 的任务,为 [CompletePartsTask] 提供 [Part] |
||||
class UploadPartsTask extends RequestTask<List<Part>> with CacheMixin { |
||||
final File file; |
||||
final String token; |
||||
final String uploadId; |
||||
|
||||
final int partSize; |
||||
final int maxPartsRequestNumber; |
||||
|
||||
final String key; |
||||
|
||||
@override |
||||
String _cacheKey; |
||||
|
||||
/// 设置为 0,避免子任务重试失败后 [UploadPartsTask] 继续重试 |
||||
@override |
||||
int get retryLimit => 0; |
||||
|
||||
// 文件 bytes 长度 |
||||
int _fileByteLength; |
||||
|
||||
// 每个上传分片的字节长度 |
||||
// |
||||
// 文件会按照此长度切片 |
||||
int _partByteLength; |
||||
|
||||
// 文件总共被拆分的分片数 |
||||
int _totalPartCount; |
||||
|
||||
// 上传成功后把 part 信息存起来 |
||||
final Map<int, Part> _uploadedPartMap = {}; |
||||
|
||||
// 处理分片上传任务的 UploadPartTask 的控制器 |
||||
final List<RequestTaskController> _workingUploadPartTaskControllers = []; |
||||
|
||||
// 已发送分片数量 |
||||
int _sentPartCount = 0; |
||||
|
||||
// 已发送到服务器的数量 |
||||
int _sentPartToServerCount = 0; |
||||
|
||||
// 剩余多少被允许的请求数 |
||||
int _idleRequestNumber; |
||||
|
||||
RandomAccessFile _raf; |
||||
|
||||
UploadPartsTask({ |
||||
@required this.file, |
||||
@required this.token, |
||||
@required this.uploadId, |
||||
@required this.partSize, |
||||
@required this.maxPartsRequestNumber, |
||||
this.key, |
||||
PutController controller, |
||||
}) : super(controller: controller); |
||||
|
||||
static String getCacheKey( |
||||
String path, |
||||
int length, |
||||
int partSize, |
||||
String key, |
||||
) { |
||||
final keyList = [ |
||||
'key/$key', |
||||
'path/$path', |
||||
'file_size/$length', |
||||
'part_size/$partSize', |
||||
]; |
||||
|
||||
return 'qiniu_dart_sdk_upload_parts_task@[${keyList..join("/")}]'; |
||||
} |
||||
|
||||
@override |
||||
void preStart() { |
||||
// 当前 controller 被取消后,所有运行中的子任务都需要被取消 |
||||
controller?.cancelToken?.whenCancel?.then((_) { |
||||
for (final controller in _workingUploadPartTaskControllers) { |
||||
controller.cancel(); |
||||
} |
||||
}); |
||||
_fileByteLength = file.lengthSync(); |
||||
_partByteLength = partSize * 1024 * 1024; |
||||
_idleRequestNumber = maxPartsRequestNumber; |
||||
_totalPartCount = (_fileByteLength / _partByteLength).ceil(); |
||||
_cacheKey = getCacheKey(file.path, _fileByteLength, partSize, key); |
||||
// 子任务 UploadPartTask 从 file 去 open 的话虽然上传精度会颗粒更细但是会导致可能读不出文件的问题 |
||||
// 可能 close 没办法立即关闭 file stream,而延迟 close 了,导致某次 open 的 stream 被立即关闭 |
||||
// 所以读不出内容了 |
||||
// 这里改成这里读取一次,子任务从中读取 bytes |
||||
_raf = file.openSync(); |
||||
super.preStart(); |
||||
} |
||||
|
||||
@override |
||||
void postReceive(data) async { |
||||
await _raf.close(); |
||||
super.postReceive(data); |
||||
} |
||||
|
||||
@override |
||||
void postError(Object error) async { |
||||
await _raf.close(); |
||||
// 取消,网络问题等可能导致上传中断,缓存已上传的分片信息 |
||||
await storeUploadedPart(); |
||||
super.postError(error); |
||||
} |
||||
|
||||
Future storeUploadedPart() async { |
||||
if (_uploadedPartMap.isEmpty) { |
||||
return; |
||||
} |
||||
|
||||
await setCache(jsonEncode(_uploadedPartMap.values.toList())); |
||||
} |
||||
|
||||
// 从缓存恢复已经上传的 part |
||||
Future recoverUploadedPart() async { |
||||
// 获取缓存 |
||||
final cachedData = await getCache(); |
||||
// 尝试从缓存恢复 |
||||
if (cachedData != null) { |
||||
var cachedList = <Part>[]; |
||||
|
||||
try { |
||||
final _cachedList = json.decode(cachedData) as List<dynamic>; |
||||
cachedList = _cachedList |
||||
.map((dynamic item) => Part.fromJson(item as Map<String, dynamic>)) |
||||
.toList(); |
||||
} catch (error) { |
||||
rethrow; |
||||
} |
||||
|
||||
for (final part in cachedList) { |
||||
_uploadedPartMap[part.partNumber] = part; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@override |
||||
Future<List<Part>> createTask() async { |
||||
/// 如果已经取消了,直接报错 |
||||
// ignore: null_aware_in_condition |
||||
if (controller != null && controller.cancelToken.isCancelled) { |
||||
throw StorageError(type: StorageErrorType.CANCEL); |
||||
} |
||||
|
||||
controller.notifyStatusListeners(StorageStatus.Request); |
||||
// 尝试恢复缓存,如果有 |
||||
await recoverUploadedPart(); |
||||
|
||||
// 上传分片 |
||||
await _uploadParts(); |
||||
return _uploadedPartMap.values.toList(); |
||||
} |
||||
|
||||
int _uploadingPartIndex = 0; |
||||
|
||||
// 从指定的分片位置往后上传切片 |
||||
Future<void> _uploadParts() async { |
||||
final tasksLength = |
||||
min(_idleRequestNumber, _totalPartCount - _uploadingPartIndex); |
||||
final taskFutures = <Future<Null>>[]; |
||||
|
||||
while (taskFutures.length < tasksLength && |
||||
_uploadingPartIndex < _totalPartCount) { |
||||
// partNumber 按照后端要求必须从 1 开始 |
||||
final partNumber = ++_uploadingPartIndex; |
||||
|
||||
final _uploadedPart = _uploadedPartMap[partNumber]; |
||||
if (_uploadedPart != null) { |
||||
_sentPartCount++; |
||||
_sentPartToServerCount++; |
||||
notifySendProgress(); |
||||
notifyProgress(); |
||||
continue; |
||||
} |
||||
|
||||
final future = _createUploadPartTaskFutureByPartNumber(partNumber); |
||||
taskFutures.add(future); |
||||
} |
||||
|
||||
await Future.wait<Null>(taskFutures); |
||||
} |
||||
|
||||
Future<Null> _createUploadPartTaskFutureByPartNumber(int partNumber) async { |
||||
// 上传分片(part)的字节大小 |
||||
final _byteLength = _getPartSizeByPartNumber(partNumber); |
||||
|
||||
_idleRequestNumber--; |
||||
final _controller = PutController(); |
||||
_workingUploadPartTaskControllers.add(_controller); |
||||
|
||||
final task = UploadPartTask( |
||||
token: token, |
||||
raf: _raf, |
||||
uploadId: uploadId, |
||||
byteLength: _byteLength, |
||||
partNumber: partNumber, |
||||
partSize: partSize, |
||||
key: key, |
||||
controller: _controller, |
||||
); |
||||
|
||||
_controller |
||||
// UploadPartTask 一次上传一个 chunk,通知一次进度 |
||||
..addSendProgressListener((percent) { |
||||
_sentPartCount++; |
||||
notifySendProgress(); |
||||
}) |
||||
// UploadPartTask 上传完成后触发 |
||||
..addProgressListener((percent) { |
||||
_sentPartToServerCount++; |
||||
notifyProgress(); |
||||
}); |
||||
|
||||
manager.addTask(task); |
||||
|
||||
final data = await task.future; |
||||
|
||||
_idleRequestNumber++; |
||||
_uploadedPartMap[partNumber] = |
||||
Part(partNumber: partNumber, etag: data.etag); |
||||
_workingUploadPartTaskControllers.remove(_controller); |
||||
|
||||
await storeUploadedPart(); |
||||
|
||||
// 检查任务是否已经完成 |
||||
if (_uploadedPartMap.length != _totalPartCount) { |
||||
// 上传下一片 |
||||
await _uploadParts(); |
||||
} |
||||
} |
||||
|
||||
// 根据 partNumber 算出当前切片的 byte 大小 |
||||
int _getPartSizeByPartNumber(int partNumber) { |
||||
final startOffset = (partNumber - 1) * _partByteLength; |
||||
|
||||
if (partNumber == _totalPartCount) { |
||||
return _fileByteLength - startOffset; |
||||
} |
||||
|
||||
return _partByteLength; |
||||
} |
||||
|
||||
void notifySendProgress() { |
||||
controller?.notifySendProgressListeners(_sentPartCount / _totalPartCount); |
||||
} |
||||
|
||||
void notifyProgress() { |
||||
controller?.notifyProgressListeners(_sentPartToServerCount / |
||||
_totalPartCount * |
||||
RequestTask.onSendProgressTakePercentOfTotal); |
||||
} |
||||
|
||||
// UploadPartsTask 自身不包含进度,在其他地方处理 |
||||
@override |
||||
void onSendProgress(double percent) {} |
||||
} |
@ -0,0 +1,13 @@
|
||||
import '../put_controller.dart'; |
||||
|
||||
class PutBySingleOptions { |
||||
/// 资源名 |
||||
/// |
||||
/// 如果不传则后端自动生成 |
||||
final String key; |
||||
|
||||
/// 控制器 |
||||
final PutController controller; |
||||
|
||||
const PutBySingleOptions({this.key, this.controller}); |
||||
} |
@ -0,0 +1,60 @@
|
||||
import 'dart:io'; |
||||
|
||||
import 'package:dio/dio.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:qiniu_sdk_base/src/storage/task/task.dart'; |
||||
|
||||
import '../../../../auth/auth.dart'; |
||||
import '../put_response.dart'; |
||||
|
||||
// 直传任务 |
||||
class PutBySingleTask extends RequestTask<PutResponse> { |
||||
/// 上传文件 |
||||
final File file; |
||||
|
||||
/// 上传凭证 |
||||
final String token; |
||||
|
||||
/// 资源名 |
||||
/// 如果不传则后端自动生成 |
||||
final String key; |
||||
|
||||
TokenInfo _tokenInfo; |
||||
|
||||
PutBySingleTask({ |
||||
@required this.file, |
||||
@required this.token, |
||||
this.key, |
||||
RequestTaskController controller, |
||||
}) : assert(file != null), |
||||
assert(token != null), |
||||
super(controller: controller); |
||||
|
||||
@override |
||||
void preStart() { |
||||
_tokenInfo = Auth.parseUpToken(token); |
||||
super.preStart(); |
||||
} |
||||
|
||||
@override |
||||
Future<PutResponse> createTask() async { |
||||
final formData = FormData.fromMap(<String, dynamic>{ |
||||
'file': await MultipartFile.fromFile(file.path), |
||||
'token': token, |
||||
'key': key, |
||||
}); |
||||
|
||||
final host = await config.hostProvider.getUpHost( |
||||
accessKey: _tokenInfo.accessKey, |
||||
bucket: _tokenInfo.putPolicy.getBucket(), |
||||
); |
||||
|
||||
final response = await client.post<Map<String, dynamic>>( |
||||
host, |
||||
data: formData, |
||||
cancelToken: controller?.cancelToken, |
||||
); |
||||
|
||||
return PutResponse.fromJson(response.data); |
||||
} |
||||
} |
@ -0,0 +1,5 @@
|
||||
export 'by_part/put_by_part_options.dart'; |
||||
export 'by_single/put_by_single_options.dart'; |
||||
export 'put_controller.dart'; |
||||
export 'put_options.dart'; |
||||
export 'put_response.dart'; |
@ -0,0 +1,3 @@
|
||||
import '../../task/request_task.dart'; |
||||
|
||||
class PutController extends RequestTaskController {} |
@ -0,0 +1,28 @@
|
||||
import 'put_controller.dart'; |
||||
|
||||
class PutOptions { |
||||
/// 资源名 |
||||
/// |
||||
/// 如果不传则后端自动生成 |
||||
final String key; |
||||
|
||||
/// 强制使用单文件上传,不使用分片,默认值 false |
||||
final bool forceBySingle; |
||||
|
||||
/// 使用分片上传时的分片大小,默认值 4,单位为 MB |
||||
final int partSize; |
||||
|
||||
/// 并发上传的队列长度,默认值为 5 |
||||
final int maxPartsRequestNumber; |
||||
|
||||
/// 控制器 |
||||
final PutController controller; |
||||
|
||||
const PutOptions({ |
||||
this.key, |
||||
this.forceBySingle = false, |
||||
this.partSize = 4, |
||||
this.maxPartsRequestNumber = 5, |
||||
this.controller, |
||||
}); |
||||
} |
@ -0,0 +1,33 @@
|
||||
import 'package:meta/meta.dart'; |
||||
|
||||
class PutResponse { |
||||
final String key; |
||||
final String hash; |
||||
|
||||
/// 如果在上传策略自定义了 [returnBody], |
||||
/// 你可以读取并解析这个字段提取你自定义的响应信息 |
||||
final Map<String, dynamic> rawData; |
||||
|
||||
PutResponse({ |
||||
@required this.key, |
||||
@required this.hash, |
||||
@required this.rawData, |
||||
}); |
||||
|
||||
factory PutResponse.fromJson(Map<String, dynamic> json) { |
||||
return PutResponse( |
||||
key: json['key'] as String, |
||||
hash: json['hash'] as String, |
||||
rawData: json, |
||||
); |
||||
} |
||||
|
||||
Map<String, dynamic> toJson() { |
||||
return <String, dynamic>{ |
||||
'key': key, |
||||
'hash': hash, |
||||
'rawData': rawData |
||||
}; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,21 @@
|
||||
enum StorageStatus { |
||||
None, |
||||
|
||||
/// 初始化任务 |
||||
Init, |
||||
|
||||
/// 请求准备发出的时候触发 |
||||
Request, |
||||
|
||||
/// 请求完成后触发 |
||||
Success, |
||||
|
||||
/// 请求被取消后触发 |
||||
Cancel, |
||||
|
||||
/// 请求出错后触发 |
||||
Error, |
||||
|
||||
/// 请求出错触发重试时触发 |
||||
Retry |
||||
} |
@ -0,0 +1,99 @@
|
||||
import 'dart:io'; |
||||
|
||||
import 'config/config.dart'; |
||||
import 'methods/put/by_part/put_parts_task.dart'; |
||||
import 'methods/put/by_single/put_by_single_task.dart'; |
||||
import 'methods/put/put.dart'; |
||||
import 'task/task.dart'; |
||||
|
||||
export 'package:dio/dio.dart' show HttpClientAdapter; |
||||
export 'config/config.dart'; |
||||
export 'error/error.dart'; |
||||
export 'methods/put/put.dart'; |
||||
export 'status/status.dart'; |
||||
export 'task/request_task.dart'; |
||||
export 'task/task.dart'; |
||||
|
||||
/// 客户端 |
||||
class Storage { |
||||
Config config; |
||||
RequestTaskManager taskManager; |
||||
|
||||
Storage({Config config}) { |
||||
this.config = config ?? Config(); |
||||
taskManager = RequestTaskManager(config: this.config); |
||||
} |
||||
|
||||
Future<PutResponse> putFile( |
||||
File file, |
||||
String token, { |
||||
PutOptions options, |
||||
}) { |
||||
options ??= PutOptions(); |
||||
RequestTask<PutResponse> task; |
||||
final useSingle = options.forceBySingle == true || |
||||
file.lengthSync() < (options.partSize * 1024 * 1024); |
||||
|
||||
if (useSingle) { |
||||
task = PutBySingleTask( |
||||
file: file, |
||||
token: token, |
||||
key: options.key, |
||||
controller: options.controller, |
||||
); |
||||
} else { |
||||
task = PutByPartTask( |
||||
file: file, |
||||
token: token, |
||||
key: options.key, |
||||
maxPartsRequestNumber: options.maxPartsRequestNumber, |
||||
partSize: options.partSize, |
||||
controller: options.controller, |
||||
); |
||||
} |
||||
|
||||
taskManager.addTask(task); |
||||
|
||||
return task.future; |
||||
} |
||||
|
||||
/// 单文件上传 |
||||
Future<PutResponse> putFileBySingle( |
||||
File file, |
||||
String token, { |
||||
PutBySingleOptions options, |
||||
}) { |
||||
options ??= PutBySingleOptions(); |
||||
final task = PutBySingleTask( |
||||
file: file, |
||||
token: token, |
||||
key: options.key, |
||||
controller: options.controller, |
||||
); |
||||
|
||||
taskManager.addTask(task); |
||||
|
||||
return task.future; |
||||
} |
||||
|
||||
/// 分片上传 |
||||
Future<PutResponse> putFileByPart( |
||||
File file, |
||||
String token, { |
||||
PutByPartOptions options, |
||||
}) { |
||||
options ??= PutByPartOptions(); |
||||
final task = PutByPartTask( |
||||
file: file, |
||||
token: token, |
||||
key: options.key, |
||||
partSize: options.partSize, |
||||
maxPartsRequestNumber: options.maxPartsRequestNumber, |
||||
controller: options.controller, |
||||
); |
||||
|
||||
taskManager.addTask(task); |
||||
|
||||
return task.future; |
||||
} |
||||
} |
@ -0,0 +1,207 @@
|
||||
import 'dart:io' show Platform; |
||||
import 'package:dio/dio.dart'; |
||||
import 'package:meta/meta.dart'; |
||||
import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; |
||||
|
||||
import 'task.dart'; |
||||
|
||||
part 'request_task_controller.dart'; |
||||
part 'request_task_manager.dart'; |
||||
|
||||
String _getUserAgent() { |
||||
return [ |
||||
'${Platform.operatingSystem}/${Platform.operatingSystemVersion}', |
||||
'Dart/${Platform.version}' |
||||
].join(' '); |
||||
} |
||||
|
||||
abstract class RequestTask<T> extends Task<T> { |
||||
// 准备阶段占总任务的百分比 |
||||
static double preStartTakePercentOfTotal = 0.001; |
||||
// 处理中阶段占总任务的百分比 |
||||
static double onSendProgressTakePercentOfTotal = 0.99; |
||||
// 完成阶段占总任务的百分比 |
||||
static double postReceiveTakePercentOfTotal = 1; |
||||
|
||||
final Dio client = Dio(); |
||||
|
||||
/// [RequestTaskManager.addTask] 会初始化这个 |
||||
Config config; |
||||
@override |
||||
// ignore: overridden_fields |
||||
covariant RequestTaskManager manager; |
||||
RequestTaskController controller; |
||||
|
||||
// 重试次数 |
||||
int _retryCount = 0; |
||||
|
||||
// 最大重试次数 |
||||
int retryLimit; |
||||
|
||||
RequestTask({this.controller}); |
||||
|
||||
@override |
||||
@mustCallSuper |
||||
void preStart() { |
||||
// 如果已经取消了,直接报错 |
||||
if (controller != null && controller.cancelToken.isCancelled) { |
||||
throw StorageError(type: StorageErrorType.CANCEL); |
||||
} |
||||
controller?.notifyStatusListeners(StorageStatus.Init); |
||||
controller?.notifyProgressListeners(preStartTakePercentOfTotal); |
||||
retryLimit = config.retryLimit; |
||||
client.httpClientAdapter = config.httpClientAdapter; |
||||
client.interceptors.add(InterceptorsWrapper(onRequest: (options) { |
||||
controller?.notifyStatusListeners(StorageStatus.Request); |
||||
options |
||||
..cancelToken = controller?.cancelToken |
||||
..onSendProgress = (sent, total) => onSendProgress(sent / total); |
||||
|
||||
return options; |
||||
})); |
||||
|
||||
client.interceptors.add(InterceptorsWrapper(onRequest: (options) { |
||||
options.headers['User-Agent'] = _getUserAgent(); |
||||
return options; |
||||
})); |
||||
|
||||
super.preStart(); |
||||
} |
||||
|
||||
@override |
||||
@mustCallSuper |
||||
void preRestart() { |
||||
controller?.notifyStatusListeners(StorageStatus.Retry); |
||||
super.preRestart(); |
||||
} |
||||
|
||||
@override |
||||
@mustCallSuper |
||||
void postReceive(T data) { |
||||
controller?.notifyStatusListeners(StorageStatus.Success); |
||||
controller?.notifyProgressListeners(postReceiveTakePercentOfTotal); |
||||
super.postReceive(data); |
||||
} |
||||
|
||||
/// [createTask] 被取消后触发 |
||||
@mustCallSuper |
||||
void postCancel(StorageError error) { |
||||
controller?.notifyStatusListeners(StorageStatus.Cancel); |
||||
} |
||||
|
||||
@override |
||||
@mustCallSuper |
||||
void postError(Object error) async { |
||||
// 处理 Dio 异常 |
||||
if (error is DioError) { |
||||
if (!_canConnectToHost(error)) { |
||||
// host 连不上,判断是否 host 不可用造成的, 比如 tls error(没做还) |
||||
if (_isHostUnavailable(error)) { |
||||
config.hostProvider.freezeHost(error.request.path); |
||||
} |
||||
|
||||
// 继续尝试当前 host,如果是服务器坏了则切换到其他 host |
||||
if (_retryCount < retryLimit) { |
||||
_retryCount++; |
||||
manager.restartTask(this); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
// 能连上但是服务器不可用,比如 502 |
||||
if (_isHostUnavailable(error)) { |
||||
config.hostProvider.freezeHost(error.request.path); |
||||
|
||||
// 切换到其他 host |
||||
if (_retryCount < retryLimit) { |
||||
_retryCount++; |
||||
manager.restartTask(this); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
final storageError = StorageError.fromDioError(error); |
||||
|
||||
// 通知状态 |
||||
if (error.type == DioErrorType.CANCEL) { |
||||
postCancel(storageError); |
||||
} else { |
||||
controller?.notifyStatusListeners(StorageStatus.Error); |
||||
} |
||||
|
||||
super.postError(storageError); |
||||
return; |
||||
} |
||||
|
||||
// 处理 Storage 异常。如果有子任务,错误可能被子任务加工成 StorageError |
||||
if (error is StorageError) { |
||||
if (error.type == StorageErrorType.CANCEL) { |
||||
postCancel(error); |
||||
} else { |
||||
controller?.notifyStatusListeners(StorageStatus.Error); |
||||
} |
||||
|
||||
super.postError(error); |
||||
return; |
||||
} |
||||
|
||||
// 不能处理的异常 |
||||
if (error is Error) { |
||||
controller?.notifyStatusListeners(StorageStatus.Error); |
||||
final storageError = StorageError.fromError(error); |
||||
super.postError(storageError); |
||||
return; |
||||
} |
||||
|
||||
controller?.notifyStatusListeners(StorageStatus.Error); |
||||
super.postError(error); |
||||
} |
||||
|
||||
// 自定义发送进度处理逻辑 |
||||
void onSendProgress(double percent) { |
||||
controller?.notifySendProgressListeners(percent); |
||||
controller |
||||
?.notifyProgressListeners(percent * onSendProgressTakePercentOfTotal); |
||||
} |
||||
|
||||
// host 是否可以连接上 |
||||
bool _canConnectToHost(Object error) { |
||||
if (error is DioError) { |
||||
if (error.type == DioErrorType.RESPONSE && |
||||
error.response.statusCode > 99) { |
||||
return true; |
||||
} |
||||
|
||||
if (error.type == DioErrorType.CANCEL) { |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
// host 是否不可用 |
||||
bool _isHostUnavailable(Object error) { |
||||
if (error is DioError) { |
||||
if (error.type == DioErrorType.RESPONSE) { |
||||
final statusCode = error.response.statusCode; |
||||
if (statusCode == 502) { |
||||
return true; |
||||
} |
||||
if (statusCode == 503) { |
||||
return true; |
||||
} |
||||
if (statusCode == 504) { |
||||
return true; |
||||
} |
||||
if (statusCode == 599) { |
||||
return true; |
||||
} |
||||
} |
||||
// ignore: todo |
||||
// TODO 更详细的信息 SocketException |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,97 @@
|
||||
part of 'request_task.dart'; |
||||
|
||||
class RequestTaskController |
||||
with |
||||
RequestTaskProgressListenersMixin, |
||||
StorageStatusListenersMixin, |
||||
RequestTaskSendProgressListenersMixin { |
||||
final CancelToken cancelToken = CancelToken(); |
||||
|
||||
/// 是否被取消过 |
||||
bool get isCancelled => cancelToken.isCancelled; |
||||
|
||||
void cancel() { |
||||
// 允许重复取消,但是已经取消后不会有任何行为发生 |
||||
if (isCancelled) { |
||||
return; |
||||
} |
||||
|
||||
cancelToken.cancel(); |
||||
} |
||||
} |
||||
|
||||
typedef RequestTaskSendProgressListener = void Function(double percent); |
||||
|
||||
/// 请求发送进度 |
||||
/// |
||||
/// 使用 Dio 发出去的请求才会触发 |
||||
mixin RequestTaskSendProgressListenersMixin { |
||||
final List<RequestTaskSendProgressListener> _sendProgressListeners = []; |
||||
|
||||
void Function() addSendProgressListener( |
||||
RequestTaskSendProgressListener listener) { |
||||
_sendProgressListeners.add(listener); |
||||
return () => removeSendProgressListener(listener); |
||||
} |
||||
|
||||
void removeSendProgressListener(RequestTaskSendProgressListener listener) { |
||||
_sendProgressListeners.remove(listener); |
||||
} |
||||
|
||||
void notifySendProgressListeners(double percent) { |
||||
for (final listener in _sendProgressListeners) { |
||||
listener(percent); |
||||
} |
||||
} |
||||
} |
||||
|
||||
typedef RequestTaskProgressListener = void Function(double percent); |
||||
|
||||
/// 任务进度 |
||||
/// |
||||
/// 当前任务的总体进度,初始化占 1%,处理请求占 98%,完成占 1%,总体 100% |
||||
mixin RequestTaskProgressListenersMixin { |
||||
final List<RequestTaskProgressListener> _progressListeners = []; |
||||
|
||||
void Function() addProgressListener(RequestTaskProgressListener listener) { |
||||
_progressListeners.add(listener); |
||||
return () => removeProgressListener(listener); |
||||
} |
||||
|
||||
void removeProgressListener(RequestTaskProgressListener listener) { |
||||
_progressListeners.remove(listener); |
||||
} |
||||
|
||||
void notifyProgressListeners(double percent) { |
||||
for (final listener in _progressListeners) { |
||||
listener(percent); |
||||
} |
||||
} |
||||
} |
||||
|
||||
typedef StorageStatusListener = void Function(StorageStatus status); |
||||
|
||||
/// 任务状态。 |
||||
/// |
||||
/// 自动触发(preStart, postReceive) |
||||
mixin StorageStatusListenersMixin { |
||||
StorageStatus status = StorageStatus.None; |
||||
|
||||
final List<StorageStatusListener> _statusListeners = []; |
||||
|
||||
void Function() addStatusListener(StorageStatusListener listener) { |
||||
_statusListeners.add(listener); |
||||
return () => removeStatusListener(listener); |
||||
} |
||||
|
||||
void removeStatusListener(StorageStatusListener listener) { |
||||
_statusListeners.remove(listener); |
||||
} |
||||
|
||||
void notifyStatusListeners(StorageStatus status) { |
||||
status = status; |
||||
for (final listener in _statusListeners) { |
||||
listener(status); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,15 @@
|
||||
part of 'request_task.dart'; |
||||
|
||||
class RequestTaskManager extends TaskManager { |
||||
final Config config; |
||||
|
||||
RequestTaskManager({ |
||||
@required this.config, |
||||
}) : assert(config != null); |
||||
|
||||
@override |
||||
void addTask(covariant RequestTask task) { |
||||
task.config = config; |
||||
super.addTask(task); |
||||
} |
||||
} |
@ -0,0 +1,50 @@
|
||||
import 'dart:async'; |
||||
|
||||
import 'package:meta/meta.dart'; |
||||
import 'package:qiniu_sdk_base/qiniu_sdk_base.dart'; |
||||
|
||||
export 'request_task.dart'; |
||||
export 'task_manager.dart'; |
||||
|
||||
/// 定义一个 Task 的抽象类 |
||||
/// |
||||
/// 异步的任务,比如请求,批处理都可以继承这个类实现一个 Task |
||||
abstract class Task<T> { |
||||
TaskManager manager; |
||||
|
||||
@protected |
||||
Completer<T> completer = Completer(); |
||||
|
||||
Future<T> get future => completer.future; |
||||
|
||||
/// 创建任务的抽象方法 |
||||
Future<T> createTask(); |
||||
|
||||
/// [Task] 启动之前会调用,该方法只会在第一次被 [TaskManager] 初始化的时候调用 |
||||
@mustCallSuper |
||||
void preStart() {} |
||||
|
||||
/// [createTask] 执行之后会调用 |
||||
@mustCallSuper |
||||
void postStart() {} |
||||
|
||||
/// 在 [createTask] 的返回值接收到结果之后调用 |
||||
@mustCallSuper |
||||
void postReceive(T data) { |
||||
manager.removeTask(this); |
||||
completer.complete(data); |
||||
} |
||||
|
||||
/// 在 [createTask] 的返回值出错之后调用 |
||||
@mustCallSuper |
||||
void postError(Object error) { |
||||
manager.removeTask(this); |
||||
completer.completeError(error); |
||||
} |
||||
|
||||
/// Task 被重启之前执行,[Task.restart] 调用后立即执行 |
||||
void preRestart() {} |
||||
|
||||
/// Task 被重启之后执行,[createTask] 被重新调用后执行 |
||||
void postRestart() {} |
||||
} |
@ -0,0 +1,66 @@
|
||||
import 'package:meta/meta.dart'; |
||||
|
||||
import 'task.dart'; |
||||
|
||||
class TaskManager { |
||||
final List<Task> workingTasks = []; |
||||
|
||||
/// 添加一个 [Task] |
||||
/// |
||||
/// 被添加的 [task] 会被立即执行 [createTask] |
||||
@mustCallSuper |
||||
void addTask(Task task) { |
||||
try { |
||||
task |
||||
..manager = this |
||||
..preStart(); |
||||
} catch (e) { |
||||
task.postError(e); |
||||
return; |
||||
} |
||||
|
||||
workingTasks.add(task); |
||||
task.createTask().then(task.postReceive).catchError(task.postError); |
||||
|
||||
try { |
||||
task.postStart(); |
||||
} catch (e) { |
||||
task.postError(e); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
@mustCallSuper |
||||
void removeTask(Task task) { |
||||
workingTasks.remove(task); |
||||
} |
||||
|
||||
@mustCallSuper |
||||
void restartTask(Task task) { |
||||
try { |
||||
task.preRestart(); |
||||
} catch (e) { |
||||
task.postError(e); |
||||
return; |
||||
} |
||||
|
||||
task.createTask().then(task.postReceive).catchError(task.postError); |
||||
|
||||
try { |
||||
task.postRestart(); |
||||
} catch (e) { |
||||
task.postError(e); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
/// 返回当前运行中的 [Task] |
||||
List<Task<dynamic>> getTasks() { |
||||
return workingTasks; |
||||
} |
||||
|
||||
/// 查找类型符合 [T] 的 [Task] |
||||
List<T> getTasksByType<T extends Task<dynamic>>() { |
||||
return workingTasks.whereType<T>().toList(); |
||||
} |
||||
} |
@ -0,0 +1,461 @@
|
||||
# Generated by pub |
||||
# See https://dart.dev/tools/pub/glossary#lockfile |
||||
packages: |
||||
_fe_analyzer_shared: |
||||
dependency: transitive |
||||
description: |
||||
name: _fe_analyzer_shared |
||||
sha256: "4d2a2c7f5e6db6acd2f4a7f713b308b45668c9573a11d0eda10936fb21fc5467" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "14.0.0" |
||||
analyzer: |
||||
dependency: transitive |
||||
description: |
||||
name: analyzer |
||||
sha256: "9fe5033adc3e8b19884e5ba296400be3794483acb53c2b68f8683744c9a7a9c7" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.41.2" |
||||
args: |
||||
dependency: transitive |
||||
description: |
||||
name: args |
||||
sha256: "6ba785824030bc97154264652acfd6a2dc699cd85f6def708fb7534d23ef1348" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.6.0" |
||||
async: |
||||
dependency: transitive |
||||
description: |
||||
name: async |
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.11.0" |
||||
boolean_selector: |
||||
dependency: transitive |
||||
description: |
||||
name: boolean_selector |
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.1" |
||||
charcode: |
||||
dependency: transitive |
||||
description: |
||||
name: charcode |
||||
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.3.1" |
||||
cli_util: |
||||
dependency: transitive |
||||
description: |
||||
name: cli_util |
||||
sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.3.5" |
||||
clock: |
||||
dependency: transitive |
||||
description: |
||||
name: clock |
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.1.1" |
||||
collection: |
||||
dependency: transitive |
||||
description: |
||||
name: collection |
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.18.0" |
||||
convert: |
||||
dependency: transitive |
||||
description: |
||||
name: convert |
||||
sha256: "3fa83bc417e26f1d55814443a603747d48e3cdf55b2f4fea27dea8e9224dcefd" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.1" |
||||
coverage: |
||||
dependency: transitive |
||||
description: |
||||
name: coverage |
||||
sha256: "7c2065a21e40c96eb0674205a48616dc4dcff138c78dfc8f46666d65b7512a46" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.14.2" |
||||
crypto: |
||||
dependency: "direct main" |
||||
description: |
||||
name: crypto |
||||
sha256: "3ce628f3c6a7144be6c524dbb20ca5af52fa59c850c587bf5e52cd70ece8e7e8" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.5" |
||||
dart2_constant: |
||||
dependency: transitive |
||||
description: |
||||
name: dart2_constant |
||||
sha256: d9791e9e1f43ab97909333f37562df8e383a906940f44700de77dd0b0a081b18 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.0.2+dart2" |
||||
dio: |
||||
dependency: "direct main" |
||||
description: |
||||
name: dio |
||||
sha256: "11979099d9ea182d74b6734340704d628b99c7a8316f9edd7718a297d1bcdd27" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.0.10" |
||||
dotenv: |
||||
dependency: "direct dev" |
||||
description: |
||||
name: dotenv |
||||
sha256: "7a906a39c0ec36c0427d5ddcf9e78004f28ffcd61be4ca5db56a823cb15e0ad2" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.0.0" |
||||
file: |
||||
dependency: transitive |
||||
description: |
||||
name: file |
||||
sha256: "781811e896666e9a728fd85b13129e32d73f05167a5da1891739f4609c6a8d7d" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "5.2.1" |
||||
glob: |
||||
dependency: transitive |
||||
description: |
||||
name: glob |
||||
sha256: f699efc018c8783b7955c74c13b1952811feb38fb6fe13f9546752a6573dde5f |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.1" |
||||
http_multi_server: |
||||
dependency: transitive |
||||
description: |
||||
name: http_multi_server |
||||
sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.2.1" |
||||
http_parser: |
||||
dependency: transitive |
||||
description: |
||||
name: http_parser |
||||
sha256: "9d2b0626e9e402fc98e6868360da8f256064d6c0b8e4c3edcca5e02fb0b95da9" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.1.4" |
||||
intl: |
||||
dependency: transitive |
||||
description: |
||||
name: intl |
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.18.1" |
||||
io: |
||||
dependency: transitive |
||||
description: |
||||
name: io |
||||
sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.0.4" |
||||
js: |
||||
dependency: transitive |
||||
description: |
||||
name: js |
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.6.7" |
||||
json_annotation: |
||||
dependency: transitive |
||||
description: |
||||
name: json_annotation |
||||
sha256: "6cec7404b25d6338c8cb7b30131cd6c760079a4ec1fa7846c55bdda91f9d2819" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.1.1" |
||||
lcov: |
||||
dependency: transitive |
||||
description: |
||||
name: lcov |
||||
sha256: "36e28d3face9c62daf00bf6861fcd6fdb579ca09a5280cbb3ac36c5b073c615e" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "5.7.0" |
||||
logging: |
||||
dependency: transitive |
||||
description: |
||||
name: logging |
||||
sha256: "239402c239e2a390cf745af3a496fb985dc8f0b31b3f5fe0d460e42289d33ad7" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.11.4" |
||||
matcher: |
||||
dependency: transitive |
||||
description: |
||||
name: matcher |
||||
sha256: "38c7be344ac5057e10161a5ecb00c9d9d67ed2f150001278601dd27d9fe64206" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.12.10" |
||||
meta: |
||||
dependency: "direct main" |
||||
description: |
||||
name: meta |
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.16.0" |
||||
mime: |
||||
dependency: transitive |
||||
description: |
||||
name: mime |
||||
sha256: a7a98ea7f366e2cc9d2b20873815aebec5e2bc124fe0da9d3f7f59b0625ea180 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.0.0" |
||||
node_interop: |
||||
dependency: transitive |
||||
description: |
||||
name: node_interop |
||||
sha256: "910d75f8589630cd197b8b7ec70e648bc380950b489b07bda1da0c5b20c4b8c1" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.1" |
||||
node_io: |
||||
dependency: transitive |
||||
description: |
||||
name: node_io |
||||
sha256: "36b74b2bcea9aa7484caa91e24bddde3f56e87fdc84f4da1cab7522a4b3680fe" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.0" |
||||
node_preamble: |
||||
dependency: transitive |
||||
description: |
||||
name: node_preamble |
||||
sha256: "847d2400938dd2594b4617244c4a9d477e939d0e134746ae88d9497e0ecb0a71" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.4.13" |
||||
package_config: |
||||
dependency: transitive |
||||
description: |
||||
name: package_config |
||||
sha256: "08b3acb5f8c9736aaf26776864ea68441bb69a82d900cbce47cd962b3ddb12c9" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.9.3" |
||||
path: |
||||
dependency: transitive |
||||
description: |
||||
name: path |
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.8.3" |
||||
pedantic: |
||||
dependency: "direct dev" |
||||
description: |
||||
name: pedantic |
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.11.1" |
||||
pool: |
||||
dependency: transitive |
||||
description: |
||||
name: pool |
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.5.1" |
||||
pub_semver: |
||||
dependency: transitive |
||||
description: |
||||
name: pub_semver |
||||
sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.4" |
||||
shelf: |
||||
dependency: transitive |
||||
description: |
||||
name: shelf |
||||
sha256: d16d1197f877e814f767be03c1c5940a397ae0ddb46dcfb9b5465074bb80ee37 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.7.9" |
||||
shelf_packages_handler: |
||||
dependency: transitive |
||||
description: |
||||
name: shelf_packages_handler |
||||
sha256: "0947da47a2d858bcf2cdf87986098808488b9c509562f55d9748abffc5cd024e" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.0.1" |
||||
shelf_static: |
||||
dependency: transitive |
||||
description: |
||||
name: shelf_static |
||||
sha256: e5ddf2266fa3d342728476a4c9718735d1e8eff1c5da1f8a3df0042f3b83c83c |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.2.9+2" |
||||
shelf_web_socket: |
||||
dependency: transitive |
||||
description: |
||||
name: shelf_web_socket |
||||
sha256: "2a300f6dd3d4a6d41941eef97920671ecd250712ce1275bf60110997691b20d1" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.2.4+1" |
||||
source_map_stack_trace: |
||||
dependency: transitive |
||||
description: |
||||
name: source_map_stack_trace |
||||
sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.1" |
||||
source_maps: |
||||
dependency: transitive |
||||
description: |
||||
name: source_maps |
||||
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.10.12" |
||||
source_span: |
||||
dependency: transitive |
||||
description: |
||||
name: source_span |
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.10.0" |
||||
stack_trace: |
||||
dependency: transitive |
||||
description: |
||||
name: stack_trace |
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.11.1" |
||||
stream_channel: |
||||
dependency: transitive |
||||
description: |
||||
name: stream_channel |
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.2" |
||||
string_scanner: |
||||
dependency: transitive |
||||
description: |
||||
name: string_scanner |
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.0" |
||||
term_glyph: |
||||
dependency: transitive |
||||
description: |
||||
name: term_glyph |
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.1" |
||||
test: |
||||
dependency: "direct dev" |
||||
description: |
||||
name: test |
||||
sha256: "53e45e20eed3f8859b3af1cb1c913a1667258d727423f46027df9a3f88822435" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.16.5" |
||||
test_api: |
||||
dependency: transitive |
||||
description: |
||||
name: test_api |
||||
sha256: "637a0c7ff81307fe6e31edfd338c798aeeef4c2c3fad031fc91ec92d5ef47c75" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.2.19" |
||||
test_core: |
||||
dependency: transitive |
||||
description: |
||||
name: test_core |
||||
sha256: "6442993591fc131006752949b0dfaaa9650243f615a71d4dfa33743ea2709767" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.3.15" |
||||
test_coverage: |
||||
dependency: "direct dev" |
||||
description: |
||||
name: test_coverage |
||||
sha256: "4c38230a26f8b7223de0a14cb0ff2257087d6f824091fa6572a472ee8bff3cb7" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.5.0" |
||||
typed_data: |
||||
dependency: transitive |
||||
description: |
||||
name: typed_data |
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.3.2" |
||||
vm_service: |
||||
dependency: transitive |
||||
description: |
||||
name: vm_service |
||||
sha256: ec8861f85bf8037ebdb453066879ece9d0f25fdac42cd2ff4d43b47239d0f8aa |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "5.5.0" |
||||
watcher: |
||||
dependency: transitive |
||||
description: |
||||
name: watcher |
||||
sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.0.2" |
||||
web_socket_channel: |
||||
dependency: transitive |
||||
description: |
||||
name: web_socket_channel |
||||
sha256: bfd93faab893a64920903d7d375517c18789c21e4c7daa5232a31ee126b7fea1 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.0" |
||||
webkit_inspection_protocol: |
||||
dependency: transitive |
||||
description: |
||||
name: webkit_inspection_protocol |
||||
sha256: "750cd8919c8ce82d399de5092f5dfce5dd295a4e52fa785540b6dbca0cef94d7" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.7.5" |
||||
yaml: |
||||
dependency: transitive |
||||
description: |
||||
name: yaml |
||||
sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.1.2" |
||||
sdks: |
||||
dart: ">=2.19.0 <3.0.0" |
@ -0,0 +1,18 @@
|
||||
name: qiniu_sdk_base |
||||
version: 0.2.2 |
||||
homepage: https://github.com/qiniu/dart-sdk |
||||
description: The sdk basic of Qiniu products |
||||
|
||||
environment: |
||||
sdk: '>=2.7.0 <3.0.0' |
||||
|
||||
dependencies: |
||||
dio: ^3.0.10 |
||||
crypto: ^2.1.5 |
||||
meta: ^1.2.3 |
||||
|
||||
dev_dependencies: |
||||
pedantic: ^1.9.0 |
||||
test: ^1.14.4 |
||||
dotenv: ^1.0.0 |
||||
test_coverage: "^0.5.0" |
@ -0,0 +1,11 @@
|
||||
coverage: |
||||
status: |
||||
# 提 pr 的时候不让 codecov 评论哪里没覆盖到 |
||||
patch: off |
||||
project: |
||||
default: |
||||
# pr 里面很多测试用例都被跳过了,所以这个 target 没有意义。如果能让需要 token 的测试在 pr 跑起来可以修改这个 target |
||||
target: 0% |
||||
branches: |
||||
- master |
||||
informational: true |
@ -0,0 +1,8 @@
|
||||
## [0.1.0] |
||||
|
||||
* Initial Release. |
||||
|
||||
## [0.2.0] |
||||
|
||||
* 优化了 `StorageError` 输出的调用栈 |
||||
* `CacheProvider` 的方法都改成异步的 |
@ -0,0 +1,201 @@
|
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright [yyyy] [name of copyright owner] |
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
you may not use this file except in compliance with the License. |
||||
You may obtain a copy of the License at |
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
||||
Unless required by applicable law or agreed to in writing, software |
||||
distributed under the License is distributed on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
See the License for the specific language governing permissions and |
||||
limitations under the License. |
@ -0,0 +1,182 @@
|
||||
# 七牛云存储 Flutter SDK [![qiniu_flutter_sdk](https://img.shields.io/pub/v/qiniu_flutter_sdk.svg?label=qiniu_flutter_sdk)](https://pub.dev/packages/qiniu_flutter_sdk) |
||||
|
||||
七牛云存储的 Flutter SDK。 |
||||
基于七牛云 API 实现,封装了七牛云存储系统的的客户端操作。 |
||||
|
||||
## 快速导航 |
||||
|
||||
* [概述](#概述) |
||||
* [示例](#示例) |
||||
* [快速开始](#快速开始) |
||||
* [功能简介](#功能简介) |
||||
* [贡献代码](#贡献代码) |
||||
* [许可证](#许可证) |
||||
|
||||
## 概述 |
||||
|
||||
Qiniu-Flutter-SDK 基于七牛云存储官方 [API](https://developer.qiniu.com/kodo) 构建,提供抽象的接口用于快速使用七牛的对象存储功能。 |
||||
|
||||
Qiniu-Flutter-SDK 为客户端 SDK,没有包含 `token` 生成实现,为了安全,`token` 建议通过网络从服务端获取,具体生成代码可以参考官方[文档](https://developer.qiniu.com/kodo/manual/1208/upload-token),我们的很多服务端 SDK 已经实现了生成 Token 的功能,推荐直接使用,[查看其他 SDK](https://developer.qiniu.com/sdk#official-sdk) |
||||
|
||||
## 示例 |
||||
|
||||
请查看 [Example](https://github.com/qiniu/dart-sdk/tree/master/flutter/example) |
||||
|
||||
## 快速开始 |
||||
|
||||
编辑你的 `pubspec.yaml` 文件,在 `dependencies` 添加 `qiniu-flutter-sdk`,如下: |
||||
|
||||
```yaml |
||||
dependencies: |
||||
... |
||||
qiniu-flutter-sdk: // 这里输入你需要的版本 |
||||
``` |
||||
|
||||
在你需要使用的地方 `import`,如下: |
||||
|
||||
```dart |
||||
import 'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart'; |
||||
``` |
||||
|
||||
### 快速使用 |
||||
|
||||
```dart |
||||
// 创建 storage 对象 |
||||
storage = Storage(); |
||||
// 使用 storage 的 putFile 对象进行文件上传 |
||||
storage.putFile(File('./file.txt'), 'TOKEN') |
||||
..then(/* 上传成功 */) |
||||
..catchError(/* 上传失败 */); |
||||
``` |
||||
|
||||
### 监听进度/状态 |
||||
|
||||
```dart |
||||
// 创建 storage 对象 |
||||
storage = Storage(); |
||||
|
||||
// 创建 Controller 对象 |
||||
putController = PutController(); |
||||
|
||||
// 添加整体进度监听 |
||||
putController.onProgress((double percent) { |
||||
print('任务进度变化:已发送:$percent'); |
||||
}); |
||||
|
||||
// 添加发送进度监听 |
||||
putController.onSendProgress((double percent) { |
||||
print('已上传进度变化:已发送:$percent'); |
||||
}); |
||||
|
||||
// 添加状态监听 |
||||
putController.addStatusListener((StorageStatus status) { |
||||
print('状态变化: 当前任务状态:$status'); |
||||
}); |
||||
|
||||
// 使用 storage 的 putFile 对象进行文件上传 |
||||
storage.putFile(File('./file.txt'), 'TOKEN', PutOptions( |
||||
controller: putController, |
||||
)) |
||||
``` |
||||
|
||||
### 取消正在上传的任务 |
||||
|
||||
```dart |
||||
// 创建 storage 对象 |
||||
storage = Storage(); |
||||
|
||||
// 创建 Controller 对象 |
||||
putController = PutController(); |
||||
|
||||
// 使用 storage 的 putFile 对象进行文件上传 |
||||
storage.putFile(File('./file.txt'), 'TOKEN', PutOptions( |
||||
controller: putController, |
||||
)) |
||||
|
||||
// 取消当前任务 |
||||
putController.cancel() |
||||
``` |
||||
|
||||
## API 说明 |
||||
|
||||
### `storage` |
||||
|
||||
使用前必须创建一个 `Storage` 实例 |
||||
|
||||
```dart |
||||
// 创建 storage 对象 |
||||
storage = Storage(); |
||||
``` |
||||
|
||||
同时,在构造 `Storage` 时可以传入一个 `Config` 控制内部的一些行为,如下: |
||||
|
||||
```dart |
||||
// 创建 storage 对象 |
||||
storage = Storage(Config( |
||||
// 通过自己的 hostProvider 来使用自己的 host 进行上传 |
||||
hostProvider: HostProvider, |
||||
// 可以通过实现 cacheProvider 来自己实现缓存系统支持分片断点续传 |
||||
cacheProvider: CacheProvider, |
||||
// 如果你需要对网络请求进行更基础的一些操作,你可以实现自己的 HttpClientAdapter 处理相关行为 |
||||
httpClientAdapter: HttpClientAdapter, |
||||
// 设定网络请求重试次数 |
||||
retryLimit: 3, |
||||
)); |
||||
``` |
||||
|
||||
#### `HostProvider` |
||||
|
||||
该接口是一个抽象的接口,大多数开发者不需要自己实现这个,除非你使用的是七牛的专/私有云服务,则可以通过实现自己的 `HostProvider` 来向自己的服务进行上传。 |
||||
|
||||
#### `CacheProvider` |
||||
|
||||
该接口同样是一个抽象的接口,`SDK` 支持分片断点续传功能,断点续传的信息通过 `CacheProvider` 提供的能力进行存储,如果你需要更好的体验,可以自己实现这个接口来对信息进行持久化的存储。 |
||||
|
||||
#### `HttpClientAdapter` |
||||
|
||||
该接口也是一个抽象的接口,如果你需要对网络请求进行进一步的自定义处理时,你可以通过实现一个 `HttpClientAdapter` 来接管 `SDK` 的所有请求。 |
||||
|
||||
#### `retryLimit` |
||||
|
||||
用于限制内部重试逻辑的重试次数, 当发生一些可重试级别的错误时,`SDK` 会使用 `retryLimit` 的次数约束自动进行尝试。 |
||||
|
||||
#### `PutController` |
||||
|
||||
这里是一个重要的内容,对于整个上传任务的一些交互被封装到了这里, |
||||
`PutController` 用于对上传任务添加进度、状态的监听,同时可以通过 `PutController.cancel()` 对正在上传的任务进行取消。使用方式可以参考:[`取消正常上传的任务`](#取消正常上传的任务) |
||||
|
||||
#### `Storage.putFile` |
||||
|
||||
该接口内部封装了分片和直传两种实现,会根据文件的尺寸和上传配置信息自动选择使用分片还是直传的方式来上传对象 |
||||
|
||||
### 其他说明 |
||||
|
||||
1. 如果您想了解更多七牛的上传策略,建议您仔细阅读 [七牛官方文档-上传](https://developer.qiniu.com/kodo/manual/upload-types)。另外,七牛的上传策略是在后端服务指定的. |
||||
|
||||
## 功能列表 |
||||
|
||||
* 单文件上传 |
||||
* 分片上传 |
||||
* 任务状态 |
||||
* 任务进度 |
||||
* 上传进度 |
||||
* 失败重试 |
||||
|
||||
## 贡献代码 |
||||
|
||||
1. 登录 https://github.com |
||||
|
||||
2. Fork git@github.com:qiniu/dart-sdk.git |
||||
|
||||
3. 创建您的特性分支 (git checkout -b new-feature) |
||||
|
||||
4. 提交您的改动 (git commit -am 'Added some features or fixed a bug') |
||||
|
||||
5. 将您的改动记录提交到远程 git 仓库 (git push origin new-feature) |
||||
|
||||
6. 然后到 github 网站的该 git 远程仓库的 new-feature 分支下发起 Pull Request |
||||
|
||||
## 许可证 |
||||
|
||||
基于 Apache 2.0 协议发布 |
||||
> Copyright (c) 2020 qiniu.com |
@ -0,0 +1,90 @@
|
||||
# copy from https://github.com/dart-lang/http/blob/master/analysis_options.yaml |
||||
|
||||
include: package:pedantic/analysis_options.yaml |
||||
|
||||
analyzer: |
||||
# enable-experiment: |
||||
# - non-nullable |
||||
|
||||
strong-mode: |
||||
implicit-casts: false |
||||
implicit-dynamic: false |
||||
|
||||
linter: |
||||
rules: |
||||
- annotate_overrides |
||||
- avoid_bool_literals_in_conditional_expressions |
||||
- avoid_classes_with_only_static_members |
||||
- avoid_empty_else |
||||
- avoid_function_literals_in_foreach_calls |
||||
- avoid_init_to_null |
||||
- avoid_null_checks_in_equality_operators |
||||
- avoid_relative_lib_imports |
||||
- avoid_renaming_method_parameters |
||||
- avoid_return_types_on_setters |
||||
- avoid_returning_null_for_void |
||||
- avoid_returning_this |
||||
- avoid_shadowing_type_parameters |
||||
- avoid_single_cascade_in_expression_statements |
||||
- avoid_types_as_parameter_names |
||||
- avoid_unused_constructor_parameters |
||||
- await_only_futures |
||||
- camel_case_types |
||||
- cascade_invocations |
||||
# comment_references |
||||
- control_flow_in_finally |
||||
- curly_braces_in_flow_control_structures |
||||
- directives_ordering |
||||
- empty_catches |
||||
- empty_constructor_bodies |
||||
- empty_statements |
||||
- file_names |
||||
- hash_and_equals |
||||
- invariant_booleans |
||||
- iterable_contains_unrelated_type |
||||
- library_names |
||||
- library_prefixes |
||||
- list_remove_unrelated_type |
||||
- no_adjacent_strings_in_list |
||||
- no_duplicate_case_values |
||||
- non_constant_identifier_names |
||||
- null_closures |
||||
- omit_local_variable_types |
||||
- only_throw_errors |
||||
- overridden_fields |
||||
- package_names |
||||
- package_prefixed_library_names |
||||
- prefer_adjacent_string_concatenation |
||||
- prefer_conditional_assignment |
||||
- prefer_contains |
||||
- prefer_equal_for_default_values |
||||
- prefer_final_fields |
||||
- prefer_collection_literals |
||||
- prefer_generic_function_type_aliases |
||||
- prefer_initializing_formals |
||||
- prefer_is_empty |
||||
- prefer_is_not_empty |
||||
- prefer_null_aware_operators |
||||
- prefer_single_quotes |
||||
- prefer_typing_uninitialized_variables |
||||
- recursive_getters |
||||
- slash_for_doc_comments |
||||
- test_types_in_equals |
||||
- throw_in_finally |
||||
- type_init_formals |
||||
- unawaited_futures |
||||
- unnecessary_brace_in_string_interps |
||||
- unnecessary_const |
||||
- unnecessary_getters_setters |
||||
- unnecessary_lambdas |
||||
- unnecessary_new |
||||
- unnecessary_null_aware_assignments |
||||
- unnecessary_null_in_if_null_operators |
||||
- unnecessary_overrides |
||||
- unnecessary_parenthesis |
||||
- unnecessary_statements |
||||
- unnecessary_this |
||||
- unrelated_type_equality_checks |
||||
- use_rethrow_when_possible |
||||
- valid_regexps |
||||
- void_checks |
@ -0,0 +1,3 @@
|
||||
library qiniu_flutter_sdk; |
||||
|
||||
export './src/storage/storage.dart'; |
@ -0,0 +1,3 @@
|
||||
import 'package:qiniu_sdk_base/qiniu_sdk_base.dart' as base; |
||||
|
||||
class PutController extends base.PutController {} |
@ -0,0 +1,33 @@
|
||||
import 'dart:io'; |
||||
|
||||
import 'package:qiniu_sdk_base/qiniu_sdk_base.dart' as base; |
||||
|
||||
export 'package:qiniu_sdk_base/qiniu_sdk_base.dart' |
||||
show |
||||
Config, |
||||
PutOptions, |
||||
PutResponse, |
||||
HostProvider, |
||||
CacheProvider, |
||||
HttpClientAdapter, |
||||
QiniuError, |
||||
StorageError, |
||||
StorageErrorType, |
||||
PutByPartOptions, |
||||
StorageStatus, |
||||
PutBySingleOptions; |
||||
|
||||
export './controller.dart'; |
||||
|
||||
class Storage { |
||||
final base.Storage _baseStorage; |
||||
Storage({base.Config config}) : _baseStorage = base.Storage(config: config); |
||||
|
||||
Future<base.PutResponse> putFile( |
||||
File file, |
||||
String token, { |
||||
base.PutOptions options, |
||||
}) { |
||||
return _baseStorage.putFile(file, token, options: options); |
||||
} |
||||
} |
@ -0,0 +1,228 @@
|
||||
# Generated by pub |
||||
# See https://dart.dev/tools/pub/glossary#lockfile |
||||
packages: |
||||
async: |
||||
dependency: transitive |
||||
description: |
||||
name: async |
||||
sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.10.0" |
||||
boolean_selector: |
||||
dependency: transitive |
||||
description: |
||||
name: boolean_selector |
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.1" |
||||
characters: |
||||
dependency: transitive |
||||
description: |
||||
name: characters |
||||
sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.1" |
||||
charcode: |
||||
dependency: transitive |
||||
description: |
||||
name: charcode |
||||
sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.3.1" |
||||
clock: |
||||
dependency: transitive |
||||
description: |
||||
name: clock |
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.1.1" |
||||
collection: |
||||
dependency: transitive |
||||
description: |
||||
name: collection |
||||
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.17.0" |
||||
convert: |
||||
dependency: transitive |
||||
description: |
||||
name: convert |
||||
sha256: "3fa83bc417e26f1d55814443a603747d48e3cdf55b2f4fea27dea8e9224dcefd" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.1" |
||||
crypto: |
||||
dependency: transitive |
||||
description: |
||||
name: crypto |
||||
sha256: "3ce628f3c6a7144be6c524dbb20ca5af52fa59c850c587bf5e52cd70ece8e7e8" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.5" |
||||
dio: |
||||
dependency: transitive |
||||
description: |
||||
name: dio |
||||
sha256: "11979099d9ea182d74b6734340704d628b99c7a8316f9edd7718a297d1bcdd27" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.0.10" |
||||
fake_async: |
||||
dependency: transitive |
||||
description: |
||||
name: fake_async |
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.3.1" |
||||
flutter: |
||||
dependency: "direct main" |
||||
description: flutter |
||||
source: sdk |
||||
version: "0.0.0" |
||||
flutter_test: |
||||
dependency: "direct dev" |
||||
description: flutter |
||||
source: sdk |
||||
version: "0.0.0" |
||||
http_parser: |
||||
dependency: transitive |
||||
description: |
||||
name: http_parser |
||||
sha256: "9d2b0626e9e402fc98e6868360da8f256064d6c0b8e4c3edcca5e02fb0b95da9" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "3.1.4" |
||||
js: |
||||
dependency: transitive |
||||
description: |
||||
name: js |
||||
sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.6.5" |
||||
matcher: |
||||
dependency: transitive |
||||
description: |
||||
name: matcher |
||||
sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.12.13" |
||||
material_color_utilities: |
||||
dependency: transitive |
||||
description: |
||||
name: material_color_utilities |
||||
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.2.0" |
||||
meta: |
||||
dependency: transitive |
||||
description: |
||||
name: meta |
||||
sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.8.0" |
||||
path: |
||||
dependency: transitive |
||||
description: |
||||
name: path |
||||
sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.8.2" |
||||
pedantic: |
||||
dependency: "direct dev" |
||||
description: |
||||
name: pedantic |
||||
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.11.1" |
||||
qiniu_sdk_base: |
||||
dependency: "direct main" |
||||
description: |
||||
path: "../base" |
||||
relative: true |
||||
source: path |
||||
version: "0.2.2" |
||||
sky_engine: |
||||
dependency: transitive |
||||
description: flutter |
||||
source: sdk |
||||
version: "0.0.99" |
||||
source_span: |
||||
dependency: transitive |
||||
description: |
||||
name: source_span |
||||
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.9.1" |
||||
stack_trace: |
||||
dependency: transitive |
||||
description: |
||||
name: stack_trace |
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.11.0" |
||||
stream_channel: |
||||
dependency: transitive |
||||
description: |
||||
name: stream_channel |
||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.1" |
||||
string_scanner: |
||||
dependency: transitive |
||||
description: |
||||
name: string_scanner |
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.0" |
||||
term_glyph: |
||||
dependency: transitive |
||||
description: |
||||
name: term_glyph |
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.2.1" |
||||
test_api: |
||||
dependency: transitive |
||||
description: |
||||
name: test_api |
||||
sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "0.4.16" |
||||
typed_data: |
||||
dependency: transitive |
||||
description: |
||||
name: typed_data |
||||
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "1.3.2" |
||||
vector_math: |
||||
dependency: transitive |
||||
description: |
||||
name: vector_math |
||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" |
||||
url: "https://pub.flutter-io.cn" |
||||
source: hosted |
||||
version: "2.1.4" |
||||
sdks: |
||||
dart: ">=2.18.0 <3.0.0" |
||||
flutter: ">=1.17.0" |
@ -0,0 +1,23 @@
|
||||
name: qiniu_flutter_sdk |
||||
description: Qiniu Flutter sdk |
||||
version: 0.2.0 |
||||
author: qiniu |
||||
homepage: https://github.com/qiniu/dart-sdk/tree/master/flutter |
||||
|
||||
environment: |
||||
sdk: ">=2.7.0 <3.0.0" |
||||
flutter: ">=1.17.0 <2.0.0" |
||||
|
||||
dependencies: |
||||
flutter: |
||||
sdk: flutter |
||||
qiniu_sdk_base: |
||||
path: ../base |
||||
|
||||
dev_dependencies: |
||||
pedantic: ^1.9.0 |
||||
flutter_test: |
||||
sdk: flutter |
||||
|
||||
|
||||
flutter: |
Loading…
Reference in new issue