Initial commit: YX OSS Flutter SDK

This commit is contained in:
Max 2025-09-26 09:56:49 +08:00
commit 5794eb93f8
30 changed files with 7435 additions and 0 deletions

View File

@ -0,0 +1,31 @@
Extension Discovery Cache
=========================
This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.
DO NOT USE THIS FOLDER
----------------------
* Do not read (or rely) the contents of this folder.
* Do write to this folder.
If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.
If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.
Use package `extension_discovery`
---------------------------------
If you want to access information from this folder.
Feel free to delete this folder
-------------------------------
Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.
Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.

View File

@ -0,0 +1 @@
{"version":2,"entries":[{"package":"yx_oss","rootUri":"../","packageUri":"lib/"}]}

View File

@ -0,0 +1 @@
{"version":2,"entries":[{"package":"yx_oss","rootUri":"../","packageUri":"lib/"}]}

View File

@ -0,0 +1,280 @@
{
"configVersion": 2,
"packages": [
{
"name": "async",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/async-2.13.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "boolean_selector",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "characters",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/characters-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "clock",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "collection",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/collection-1.19.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "crypto",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/crypto-3.0.6",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "dio",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/dio-5.9.0",
"packageUri": "lib/",
"languageVersion": "2.18"
},
{
"name": "dio_web_adapter",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/dio_web_adapter-2.1.1",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "fake_async",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.3.3",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "ffi",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/ffi-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "flutter",
"rootUri": "file:///Users/max/fvm/versions/3.32.0/packages/flutter",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "flutter_lints",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/flutter_lints-4.0.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "flutter_test",
"rootUri": "file:///Users/max/fvm/versions/3.32.0/packages/flutter_test",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "http_parser",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/http_parser-4.1.2",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "leak_tracker",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker-10.0.9",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_flutter_testing",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_flutter_testing-3.0.9",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "leak_tracker_testing",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_testing-3.0.1",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "lints",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/lints-4.0.0",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "matcher",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.17",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "material_color_utilities",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/material_color_utilities-0.11.1",
"packageUri": "lib/",
"languageVersion": "2.17"
},
{
"name": "meta",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/meta-1.16.0",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "mime",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/mime-1.0.6",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "path",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "path_provider",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.1.5",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "path_provider_android",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_android-2.2.18",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "path_provider_foundation",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.4.2",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "path_provider_linux",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.2.1",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "path_provider_platform_interface",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_platform_interface-2.1.2",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "path_provider_windows",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.3.0",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "platform",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/platform-3.1.6",
"packageUri": "lib/",
"languageVersion": "3.2"
},
{
"name": "plugin_platform_interface",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/plugin_platform_interface-2.1.8",
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "sky_engine",
"rootUri": "file:///Users/max/fvm/versions/3.32.0/bin/cache/pkg/sky_engine",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "source_span",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "stack_trace",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.12.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "stream_channel",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.4",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "string_scanner",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.4.1",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "term_glyph",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.2",
"packageUri": "lib/",
"languageVersion": "3.1"
},
{
"name": "test_api",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.7.4",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "typed_data",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.4.0",
"packageUri": "lib/",
"languageVersion": "3.5"
},
{
"name": "uuid",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/uuid-3.0.7",
"packageUri": "lib/",
"languageVersion": "2.12"
},
{
"name": "vector_math",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.4",
"packageUri": "lib/",
"languageVersion": "2.14"
},
{
"name": "vm_service",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/vm_service-15.0.0",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "web",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/web-1.1.1",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "xdg_directories",
"rootUri": "file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "yx_oss",
"rootUri": "../",
"packageUri": "lib/",
"languageVersion": "3.0"
}
],
"generator": "pub",
"generatorVersion": "3.8.0",
"flutterRoot": "file:///Users/max/fvm/versions/3.32.0",
"flutterVersion": "3.32.0",
"pubCache": "file:///Users/max/.pub-cache"
}

View File

@ -0,0 +1,181 @@
async
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/async-2.13.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/async-2.13.0/lib/
boolean_selector
3.1
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.2/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.2/lib/
characters
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/characters-1.4.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/characters-1.4.0/lib/
clock
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.2/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.2/lib/
collection
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/collection-1.19.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/collection-1.19.1/lib/
crypto
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/crypto-3.0.6/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/crypto-3.0.6/lib/
dio
2.18
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/dio-5.9.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/dio-5.9.0/lib/
dio_web_adapter
3.3
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/dio_web_adapter-2.1.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/dio_web_adapter-2.1.1/lib/
fake_async
3.3
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.3.3/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.3.3/lib/
ffi
3.7
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/ffi-2.1.4/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/ffi-2.1.4/lib/
flutter_lints
3.1
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/flutter_lints-4.0.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/flutter_lints-4.0.0/lib/
http_parser
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/http_parser-4.1.2/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/http_parser-4.1.2/lib/
leak_tracker
3.2
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker-10.0.9/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker-10.0.9/lib/
leak_tracker_flutter_testing
3.2
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_flutter_testing-3.0.9/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_flutter_testing-3.0.9/lib/
leak_tracker_testing
3.2
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_testing-3.0.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/leak_tracker_testing-3.0.1/lib/
lints
3.1
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/lints-4.0.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/lints-4.0.0/lib/
matcher
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.17/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.17/lib/
material_color_utilities
2.17
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/material_color_utilities-0.11.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/material_color_utilities-0.11.1/lib/
meta
2.12
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/meta-1.16.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/meta-1.16.0/lib/
mime
3.2
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/mime-1.0.6/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/mime-1.0.6/lib/
path
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path-1.9.1/lib/
path_provider
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.1.5/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.1.5/lib/
path_provider_android
3.7
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_android-2.2.18/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_android-2.2.18/lib/
path_provider_foundation
3.7
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.4.2/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.4.2/lib/
path_provider_linux
2.19
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.2.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.2.1/lib/
path_provider_platform_interface
3.0
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_platform_interface-2.1.2/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_platform_interface-2.1.2/lib/
path_provider_windows
3.2
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.3.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.3.0/lib/
platform
3.2
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/platform-3.1.6/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/platform-3.1.6/lib/
plugin_platform_interface
3.0
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/plugin_platform_interface-2.1.8/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/plugin_platform_interface-2.1.8/lib/
source_span
3.1
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.10.1/lib/
stack_trace
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.12.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.12.1/lib/
stream_channel
3.3
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.4/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.4/lib/
string_scanner
3.1
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.4.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.4.1/lib/
term_glyph
3.1
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.2/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.2/lib/
test_api
3.5
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.7.4/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.7.4/lib/
typed_data
3.5
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.4.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.4.0/lib/
uuid
2.12
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/uuid-3.0.7/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/uuid-3.0.7/lib/
vector_math
2.14
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.4/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.4/lib/
vm_service
3.3
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/vm_service-15.0.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/vm_service-15.0.0/lib/
web
3.4
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/web-1.1.1/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/web-1.1.1/lib/
xdg_directories
3.3
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0/
file:///Users/max/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0/lib/
yx_oss
3.0
file:///Users/max/SourceCode/yuanxuan/learning_officer_oa/yx_oss/
file:///Users/max/SourceCode/yuanxuan/learning_officer_oa/yx_oss/lib/
sky_engine
3.7
file:///Users/max/fvm/versions/3.32.0/bin/cache/pkg/sky_engine/
file:///Users/max/fvm/versions/3.32.0/bin/cache/pkg/sky_engine/lib/
flutter
3.7
file:///Users/max/fvm/versions/3.32.0/packages/flutter/
file:///Users/max/fvm/versions/3.32.0/packages/flutter/lib/
flutter_test
3.7
file:///Users/max/fvm/versions/3.32.0/packages/flutter_test/
file:///Users/max/fvm/versions/3.32.0/packages/flutter_test/lib/
2

View File

@ -0,0 +1,387 @@
{
"roots": [
"yx_oss"
],
"packages": [
{
"name": "yx_oss",
"version": "1.0.0",
"dependencies": [
"crypto",
"dio",
"flutter",
"meta",
"mime",
"path_provider",
"uuid"
],
"devDependencies": [
"flutter_lints",
"flutter_test"
]
},
{
"name": "flutter_lints",
"version": "4.0.0",
"dependencies": [
"lints"
]
},
{
"name": "flutter_test",
"version": "0.0.0",
"dependencies": [
"async",
"boolean_selector",
"characters",
"clock",
"collection",
"fake_async",
"flutter",
"leak_tracker",
"leak_tracker_flutter_testing",
"leak_tracker_testing",
"matcher",
"material_color_utilities",
"meta",
"path",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph",
"test_api",
"vector_math",
"vm_service"
]
},
{
"name": "path_provider",
"version": "2.1.5",
"dependencies": [
"flutter",
"path_provider_android",
"path_provider_foundation",
"path_provider_linux",
"path_provider_platform_interface",
"path_provider_windows"
]
},
{
"name": "meta",
"version": "1.16.0",
"dependencies": []
},
{
"name": "uuid",
"version": "3.0.7",
"dependencies": [
"crypto"
]
},
{
"name": "mime",
"version": "1.0.6",
"dependencies": []
},
{
"name": "crypto",
"version": "3.0.6",
"dependencies": [
"typed_data"
]
},
{
"name": "dio",
"version": "5.9.0",
"dependencies": [
"async",
"collection",
"dio_web_adapter",
"http_parser",
"meta",
"mime",
"path"
]
},
{
"name": "flutter",
"version": "0.0.0",
"dependencies": [
"characters",
"collection",
"material_color_utilities",
"meta",
"sky_engine",
"vector_math"
]
},
{
"name": "lints",
"version": "4.0.0",
"dependencies": []
},
{
"name": "vm_service",
"version": "15.0.0",
"dependencies": []
},
{
"name": "term_glyph",
"version": "1.2.2",
"dependencies": []
},
{
"name": "string_scanner",
"version": "1.4.1",
"dependencies": [
"source_span"
]
},
{
"name": "stream_channel",
"version": "2.1.4",
"dependencies": [
"async"
]
},
{
"name": "source_span",
"version": "1.10.1",
"dependencies": [
"collection",
"path",
"term_glyph"
]
},
{
"name": "material_color_utilities",
"version": "0.11.1",
"dependencies": [
"collection"
]
},
{
"name": "leak_tracker_testing",
"version": "3.0.1",
"dependencies": [
"leak_tracker",
"matcher",
"meta"
]
},
{
"name": "leak_tracker",
"version": "10.0.9",
"dependencies": [
"clock",
"collection",
"meta",
"path",
"vm_service"
]
},
{
"name": "collection",
"version": "1.19.1",
"dependencies": []
},
{
"name": "characters",
"version": "1.4.0",
"dependencies": []
},
{
"name": "boolean_selector",
"version": "2.1.2",
"dependencies": [
"source_span",
"string_scanner"
]
},
{
"name": "async",
"version": "2.13.0",
"dependencies": [
"collection",
"meta"
]
},
{
"name": "leak_tracker_flutter_testing",
"version": "3.0.9",
"dependencies": [
"flutter",
"leak_tracker",
"leak_tracker_testing",
"matcher",
"meta"
]
},
{
"name": "vector_math",
"version": "2.1.4",
"dependencies": []
},
{
"name": "stack_trace",
"version": "1.12.1",
"dependencies": [
"path"
]
},
{
"name": "clock",
"version": "1.1.2",
"dependencies": []
},
{
"name": "fake_async",
"version": "1.3.3",
"dependencies": [
"clock",
"collection"
]
},
{
"name": "path",
"version": "1.9.1",
"dependencies": []
},
{
"name": "matcher",
"version": "0.12.17",
"dependencies": [
"async",
"meta",
"stack_trace",
"term_glyph",
"test_api"
]
},
{
"name": "test_api",
"version": "0.7.4",
"dependencies": [
"async",
"boolean_selector",
"collection",
"meta",
"source_span",
"stack_trace",
"stream_channel",
"string_scanner",
"term_glyph"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_platform_interface",
"version": "2.1.2",
"dependencies": [
"flutter",
"platform",
"plugin_platform_interface"
]
},
{
"name": "path_provider_linux",
"version": "2.2.1",
"dependencies": [
"ffi",
"flutter",
"path",
"path_provider_platform_interface",
"xdg_directories"
]
},
{
"name": "path_provider_foundation",
"version": "2.4.2",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "path_provider_android",
"version": "2.2.18",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "typed_data",
"version": "1.4.0",
"dependencies": [
"collection"
]
},
{
"name": "dio_web_adapter",
"version": "2.1.1",
"dependencies": [
"dio",
"http_parser",
"meta",
"web"
]
},
{
"name": "http_parser",
"version": "4.1.2",
"dependencies": [
"collection",
"source_span",
"string_scanner",
"typed_data"
]
},
{
"name": "sky_engine",
"version": "0.0.0",
"dependencies": []
},
{
"name": "ffi",
"version": "2.1.4",
"dependencies": []
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "platform",
"version": "3.1.6",
"dependencies": []
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "web",
"version": "1.1.1",
"dependencies": []
}
],
"configVersion": 1
}

1
.dart_tool/version Normal file
View File

@ -0,0 +1 @@
3.32.0

View File

@ -0,0 +1 @@
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"/Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"path_provider_android","path":"/Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_android-2.2.18/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"path_provider_foundation","path":"/Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"path_provider_linux","path":"/Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"path_provider_windows","path":"/Users/max/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false}],"web":[]},"dependencyGraph":[{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2025-09-24 16:33:04.709702","version":"3.32.0","swift_package_manager_enabled":{"ios":false,"macos":false}}

572
README.md Normal file
View File

@ -0,0 +1,572 @@
# YX OSS
[![pub package](https://img.shields.io/pub/v/yx_oss.svg)](https://pub.dev/packages/yx_oss)
[![documentation](https://img.shields.io/badge/documentation-latest-brightgreen.svg)](https://pub.dev/documentation/yx_oss/latest/)
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
一个专注于核心功能的阿里云 OSS (Object Storage Service) Flutter 客户端库,提供纯净的文件上传下载能力。
## 🎯 设计理念
**单一职责**:本库专注于 OSS 核心操作 - 文件上传、下载和管理。故意排除了 UI 组件、图片压缩、文件选择等功能,保持简洁性和灵活性。
**最小依赖**:仅包含 OSS 操作的必需依赖,让您可以自由选择自己的 UI 和工具库。
## ✨ 核心特性
### 认证与配置
- 🔐 **多种认证方式**静态密钥、STS临时凭证、预签名URL
- ⚙️ **动态配置**:运行时配置更新和缓存
- 🔌 **可插拔提供者**:灵活的认证和配置提供者架构
### 上传与下载
- 📤 **断点续传**:支持大文件的断点续传,本地存储断点信息
- 🧩 **分片上传**:大文件自动分片处理,支持并发上传
- 📊 **进度监控**:实时上传下载进度跟踪
- ⚡ **预签名URL**:支持客户端直接上传,服务端控制权限
### 可靠性与错误处理
- 🔄 **重试机制**:指数退避重试失败操作
- 🛡️ **类型安全错误处理**:完整的错误类型和处理
- ❌ **取消支持**:可取消正在进行的操作
- 📝 **统一日志**:可配置的日志系统,支持自定义处理器
### 架构特点
- 🎯 **零耦合**:不依赖特定项目架构
- 📱 **跨平台**:支持 iOS、Android、Web、Desktop
- 🏗️ **清洁架构**:依赖注入和关注点分离
- 🔧 **项目集成**:提供现成的项目集成适配器
## 🚀 快速开始
### 安装
在你的 `pubspec.yaml` 文件中添加依赖:
```yaml
dependencies:
yx_oss: ^1.0.0
```
然后运行:
```bash
flutter pub get
```
### 基础用法
```dart
import 'package:yx_oss/yx_oss.dart';
// 1. 创建认证提供者
final authProvider = StaticAuthProvider(
accessKeyId: 'your_access_key_id',
accessKeySecret: 'your_access_key_secret',
);
// 2. 创建配置提供者
final configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: 'uploads',
);
// 3. 创建客户端配置
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
logConfig: LogConfig(
enabled: true,
level: LogLevel.info,
logError: true,
),
);
// 4. 创建OSS客户端
final client = YxOSSClient(config);
// 5. 初始化客户端
await client.initialize();
// 6. 上传文件
final fileData = Uint8List.fromList('Hello, World!'.codeUnits);
final result = await client.uploadBytes(
fileData,
'my-file.txt',
options: UploadOptions(
callbacks: UploadCallbacks(
onProgress: (sent, total) {
print('进度: ${(sent / total * 100).toStringAsFixed(1)}%');
},
onSuccess: (result) {
print('上传成功: ${result.url}');
},
),
),
);
// 7. 释放资源
client.dispose();
```
## 📖 详细文档
### 认证方式
#### 1. 静态认证 (Static Authentication)
适用于开发测试环境或简单应用场景:
```dart
final authProvider = StaticAuthProvider(
accessKeyId: 'your_access_key_id',
accessKeySecret: 'your_access_key_secret',
);
```
#### 2. STS临时凭证 (STS Authentication)
适用于生产环境,提供更安全的权限控制:
```dart
// 从服务器获取STS凭证的函数
Future<STSCredentials> getSTSCredentials() async {
// 调用你的后端API获取STS凭证
final response = await http.get('/api/sts-credentials');
return STSCredentials.fromJson(response.data);
}
final authProvider = STSAuthProvider(
accessKeyId: stsCredentials.accessKeyId,
accessKeySecret: stsCredentials.accessKeySecret,
securityToken: stsCredentials.securityToken,
expiration: stsCredentials.expiration,
credentialsGetter: getSTSCredentials, // 自动刷新过期凭证
);
```
#### 3. 项目集成认证 (Project Integration)
适用于现有项目集成从后端API获取配置
```dart
// 使用项目集成适配器
final manager = ProjectOSSManager(myApiImpl);
await manager.initialize();
// 直接使用,自动处理认证和配置
final fileUrl = await manager.uploadFile(
filePath,
onProgress: (sent, total) => print('进度: $sent/$total'),
type: 1, // 文件类型
);
```
### 配置管理
#### 1. 静态配置 (Static Configuration)
```dart
final configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: 'uploads',
domain: 'https://your-custom-domain.com', // 可选
);
```
#### 2. 动态配置 (Dynamic Configuration)
支持从服务器动态获取配置,适用于多环境部署:
```dart
final configProvider = DynamicConfigProvider(
configGetter: () async {
final response = await http.get('/api/oss-config');
return OSSConfig.fromJson(response.data);
},
cacheTimeout: const Duration(hours: 1), // 配置缓存时间
);
```
### 日志配置
新增统一的日志系统,支持级别控制和自定义处理器:
```dart
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
logConfig: LogConfig(
enabled: true,
level: LogLevel.warning, // 只记录警告及以上级别
logRequest: false, // 是否记录HTTP请求
logResponse: false, // 是否记录HTTP响应
logError: true, // 是否记录错误
customHandler: (message, level) {
// 自定义日志处理,可接入你的日志系统
myLogger.log(level.name, message);
},
),
);
```
### 上传选项
```dart
final options = UploadOptions(
// 基础选项
overwrite: true, // 是否覆盖同名文件
contentType: 'image/jpeg', // 文件MIME类型
enableResume: true, // 启用断点续传
// 访问控制
acl: ACLMode.publicRead, // 访问权限
storageClass: StorageClass.standard, // 存储类型
// 自定义元数据
metadata: {
'author': 'YX OSS',
'version': '1.0.0',
},
// 分片上传配置
enableMultipart: true, // 启用分片上传
partSize: 10 * 1024 * 1024, // 分片大小 (10MB)
multipartThreshold: 100 * 1024 * 1024, // 分片阈值 (100MB)
concurrency: 3, // 并发上传数
// 回调函数
callbacks: UploadCallbacks(
onStart: () => print('开始上传'),
onProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
print('上传进度: $progress%');
},
onSuccess: (result) => print('上传成功: ${result.url}'),
onError: (error) => print('上传失败: ${error.message}'),
onComplete: () => print('上传完成'),
),
);
```
### 断点续传
支持大文件的断点续传功能:
```dart
// 启用断点续传的上传
final result = await client.uploadFile(
largeFilePath,
options: UploadOptions(
enableResume: true,
callbacks: UploadCallbacks(
onProgress: (sent, total) {
print('断点续传进度: ${(sent/total*100).toInt()}%');
},
),
),
);
// 获取可恢复的上传任务
final resumableTasks = await client.getResumableTasks();
for (final task in resumableTasks) {
print('可恢复任务: ${task.objectKey}, 进度: ${task.uploadedSize}/${task.totalSize}');
}
// 恢复指定任务
final resumeResult = await client.resumeUpload(taskId);
// 清理过期的断点续传数据
await client.cleanupExpiredData(maxAgeDays: 7);
```
### 错误处理
库提供了完整的错误类型定义:
```dart
try {
final result = await client.uploadBytes(data, 'file.txt');
print('上传成功: ${result.url}');
} catch (e) {
if (e is OSSError) {
switch (e.type) {
case OSSErrorType.network:
print('网络错误: ${e.message}');
break;
case OSSErrorType.authentication:
print('认证错误: ${e.message}');
break;
case OSSErrorType.permission:
print('权限错误: ${e.message}');
break;
case OSSErrorType.server:
print('服务器错误: ${e.message}');
break;
default:
print('其他错误: ${e.message}');
}
// 检查是否可重试
if (e.isRetryable) {
print('此错误可以重试');
}
}
}
```
## 🔧 高级配置
### 完整配置示例
```dart
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
// 超时配置
timeoutConfig: TimeoutConfig(
connectTimeout: 30000, // 30秒连接超时
sendTimeout: 600000, // 10分钟发送超时
receiveTimeout: 600000, // 10分钟接收超时
),
// 重试配置
retryConfig: RetryConfig(
maxRetries: 5, // 最多重试5次
initialInterval: 500, // 初始间隔500ms
backoffMultiplier: 2.0, // 指数退避因子
maxInterval: 30000, // 最大间隔30秒
enableExponentialBackoff: true, // 启用指数退避
),
// 日志配置
logConfig: LogConfig(
enabled: true,
level: LogLevel.debug,
logRequest: true, // 记录请求详情
logResponse: true, // 记录响应详情
logError: true, // 记录错误详情
customHandler: (message, level) {
// 自定义日志处理
print('[YX_OSS] ${level.name.toUpperCase()}: $message');
},
),
);
```
## 🏗️ 项目集成
### 使用项目集成适配器
库提供了现成的项目集成适配器,可以快速集成到现有项目中:
```dart
// 1. 实现服务器API接口
class MyOSSApiImpl implements YxOssServerInterface {
@override
Future<YxOSSConfigModel> getOssConfig() async {
// 从你的后端获取OSS配置
final response = await http.get('/api/oss-config');
return YxOSSConfigModel.fromJson(response.data);
}
@override
Future<YxOSSSignModel> getOssSign({String? objectName, int? type}) async {
// 从你的后端获取预签名URL
final response = await http.post('/api/oss-sign', {
'objectName': objectName,
'type': type,
});
return YxOSSSignModel.fromJson(response.data);
}
@override
Future<bool> deleteOSSFile({required String filePath}) async {
// 删除文件
final response = await http.delete('/api/oss-file', {
'filePath': filePath,
});
return response.statusCode == 200;
}
}
// 2. 使用项目集成管理器
final ossManager = ProjectOSSManager(MyOSSApiImpl());
await ossManager.initialize();
// 3. 上传文件自动使用预签名URL
final fileUrl = await ossManager.uploadFile(
filePath,
onProgress: (sent, total) {
print('上传进度: ${(sent/total*100).toInt()}%');
},
type: 1, // 文件类型1-普通上传2-资料收集等
);
// 4. 上传字节数组
final fileUrl = await ossManager.uploadBytes(
fileBytes,
'example.jpg',
onProgress: (sent, total) => print('进度: $sent/$total'),
type: 1,
);
// 5. 删除文件
final success = await ossManager.deleteFile(fileUrl);
// 6. 断点续传相关
final tasks = await ossManager.getResumableTasks();
final resumeResult = await ossManager.resumeUpload(taskId);
await ossManager.cleanupExpiredData(maxAgeDays: 7);
```
### 集成到应用生命周期
```dart
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final ProjectOSSManager ossManager;
@override
void initState() {
super.initState();
ossManager = ProjectOSSManager(MyOSSApiImpl());
}
Future<void> initializeOSS() async {
try {
await ossManager.initialize();
print('OSS初始化成功');
} catch (e) {
print('OSS初始化失败: $e');
}
}
@override
void dispose() {
ossManager.dispose();
super.dispose();
}
// ... 其他代码
}
```
## 🌍 区域支持
库内置了常用的阿里云OSS区域配置
```dart
// 中国大陆区域
CommonRegions.hangzhou // 华东1 (杭州)
CommonRegions.beijing // 华北2 (北京)
CommonRegions.shanghai // 华东2 (上海)
CommonRegions.shenzhen // 华南1 (深圳)
CommonRegions.chengdu // 西南1 (成都)
// 海外区域
CommonRegions.singapore // 亚太东南1 (新加坡)
CommonRegions.sydney // 亚太东南2 (悉尼)
CommonRegions.tokyo // 亚太东北1 (东京)
CommonRegions.virginia // 美国东部1 (弗吉尼亚)
CommonRegions.london // 欧洲西部1 (伦敦)
```
## 🔄 迁移指南
### 从Legacy OSS迁移
如果你正在从现有的OSS实现迁移到YX OSS
#### 原项目代码
```dart
// 原项目可能是这样的
final ossClient = AliyunOssClientUtils();
await ossClient.uploadFile(file, fileName);
```
#### YX OSS代码
```dart
// 迁移到YX OSS - 使用项目集成适配器
final ossManager = ProjectOSSManager(apiImpl);
await ossManager.initialize();
final fileUrl = await ossManager.uploadFile(filePath);
```
#### 迁移优势
1. **解耦合**: YX OSS是完全独立的库不依赖项目特定的代码
2. **更好的错误处理**: 类型安全的错误处理机制
3. **断点续传**: 完整的断点续传支持
4. **灵活配置**: 支持多种认证和配置方式
5. **统一日志**: 可配置的日志系统
6. **更好的测试**: 所有组件都可以独立测试
## 📚 API文档
### 核心类
- `YxOSSClient` - OSS客户端主类
- `YxOSSConfig` - 客户端配置
- `ProjectOSSManager` - 项目集成管理器
### 接口
- `AuthProvider` - 认证提供者接口
- `ConfigProvider` - 配置提供者接口
- `ResumeStorage` - 断点续传存储接口
- `YxOssServerInterface` - 服务器API接口
### 模型
- `UploadOptions` - 上传选项
- `UploadResult` - 上传结果
- `OSSError` - 错误类型
### 工具
- `YxOSSFileUtils` - 文件工具类
- `OssLogger` - 日志工具
## 🧪 测试
```dart
// 运行测试
flutter test
// 运行集成测试
flutter test integration_test/
```
### 开发环境设置
1. Fork 项目
2. 创建功能分支
3. 提交更改
4. 创建 Pull Request
## 📈 更新日志
### v1.0.0
- ✅ 核心OSS上传下载功能
- ✅ 多种认证方式支持
- ✅ 断点续传功能
- ✅ 项目集成适配器
- ✅ 统一日志系统
- ✅ 完整的错误处理
- ✅ 类型安全的API设计
---
**YX OSS** - 让 Flutter OSS 上传更简单、更可靠! 🚀

534
example/main.dart Normal file
View File

@ -0,0 +1,534 @@
import 'dart:typed_data';
import 'package:yx_oss/yx_oss.dart';
/// YX OSS 使
///
/// 使
void main() async {
print('YX OSS 使用示例');
// 1: +
await example1StaticAuth();
// 2: STS临时凭证认证
await example2STSAuth();
// 3: URL认证
await example3PreSignedAuth();
// 4:
await example4DynamicConfig();
// 5:
await example5MultipartUpload();
}
/// 1: +
/// :
Future<void> example1StaticAuth() async {
print('\n=== 示例1: 静态配置 + 静态认证 ===');
//
const authProvider = StaticAuthProvider(
accessKeyId: 'your_access_key_id',
accessKeySecret: 'your_access_key_secret',
);
//
const configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: 'uploads', // 'uploads'
domain: 'https://your-custom-domain.com', //
);
// OSS客户端配置
const config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
//
timeoutConfig: TimeoutConfig(
connectTimeout: 60000,
sendTimeout: 300000,
receiveTimeout: 300000,
),
//
retryConfig: RetryConfig(
maxRetries: 3,
initialInterval: 1000,
enableExponentialBackoff: true,
),
//
logConfig: LogConfig(
enabled: true,
level: LogLevel.info,
logRequest: false,
logResponse: false,
logError: true,
),
);
// OSS客户端
final client = YxOSSClient(config);
try {
//
await client.initialize();
//
final fileData = Uint8List.fromList('Hello, YX OSS!'.codeUnits);
//
final options = UploadOptions(
overwrite: true,
acl: ACLMode.publicRead,
storageClass: StorageClass.standard,
contentType: 'text/plain',
metadata: {'author': 'YX OSS Example'},
callbacks: UploadCallbacks(
onStart: () => print('开始上传...'),
onProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
print('上传进度: $progress% ($sent/$total)');
},
onSuccess: (result) => print('上传成功: ${result.url}'),
onError: (error) => print('上传失败: $error'),
onComplete: () => print('上传完成'),
),
);
//
final result = await client.uploadBytes(
fileData,
'examples/test-file.txt',
options: options,
);
print('上传结果:');
print(' URL: ${result.url}');
print(' 对象键: ${result.objectKey}');
print(' 文件大小: ${result.fileSize} 字节');
print(' ETag: ${result.etag}');
print(' 上传耗时: ${result.uploadDuration} 毫秒');
} catch (e) {
print('操作失败: $e');
} finally {
//
client.dispose();
}
}
/// 2: STS临时凭证认证
/// :
Future<void> example2STSAuth() async {
print('\n=== 示例2: STS临时凭证认证 ===');
// STS凭证的函数
Future<STSCredentials> getSTSCredentials() async {
// API获取STS凭证
//
return STSCredentials(
accessKeyId: 'STS.your_sts_access_key_id',
accessKeySecret: 'your_sts_access_key_secret',
securityToken: 'your_security_token',
expiration: DateTime.now().add(const Duration(hours: 1)),
);
}
// STS认证提供者
final stsCredentials = await getSTSCredentials();
final authProvider = STSAuthProvider(
accessKeyId: stsCredentials.accessKeyId,
accessKeySecret: stsCredentials.accessKeySecret,
securityToken: stsCredentials.securityToken,
expiration: stsCredentials.expiration,
credentialsGetter: getSTSCredentials, //
);
const configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: 'sts-uploads',
);
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
);
final client = YxOSSClient(config);
try {
await client.initialize();
final fileData = Uint8List.fromList('STS upload test'.codeUnits);
final result = await client.uploadBytes(
fileData,
'sts-test-file.txt',
options: UploadOptions(
callbacks: UploadCallbacks(
onProgress: (sent, total) {
print('STS上传进度: ${(sent / total * 100).toStringAsFixed(1)}%');
},
),
),
);
print('STS上传成功: ${result.url}');
} catch (e) {
print('STS上传失败: $e');
} finally {
client.dispose();
}
}
/// 3: URL认证
/// :
Future<void> example3PreSignedAuth() async {
print('\n=== 示例3: 预签名URL认证 ===');
// URL的函数
Future<String> getPreSignedUrl(String fileName) async {
// API获取预签名URL
// URL
return 'https://your-bucket.oss-cn-hangzhou.aliyuncs.com/uploads/$fileName?Expires=1234567890&OSSAccessKeyId=your_key&Signature=signature';
}
// URL认证提供者
final authProvider = PreSignedAuthProvider(
urlGetter: getPreSignedUrl,
);
// URL模式下使
const configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: '', // URL通常已包含完整路径
);
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
);
final client = YxOSSClient(config);
try {
await client.initialize();
final fileData = Uint8List.fromList('PreSigned URL upload test'.codeUnits);
final result = await client.uploadBytes(
fileData,
'presigned-test-file.txt',
options: UploadOptions(
callbacks: UploadCallbacks(
onProgress: (sent, total) {
print('预签名上传进度: ${(sent / total * 100).toStringAsFixed(1)}%');
},
),
),
);
print('预签名上传成功: ${result.url}');
} catch (e) {
print('预签名上传失败: $e');
} finally {
client.dispose();
}
}
/// 4:
/// :
Future<void> example4DynamicConfig() async {
print('\n=== 示例4: 动态配置 ===');
// OSS配置的函数
Future<OSSConfig> getOSSConfig() async {
// API获取OSS配置
return const OSSConfig(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'dynamic-bucket-name',
directory: 'dynamic-uploads',
domain: 'https://cdn.example.com',
);
}
const authProvider = StaticAuthProvider(
accessKeyId: 'your_access_key_id',
accessKeySecret: 'your_access_key_secret',
);
//
final configProvider = DynamicConfigProvider(
configGetter: getOSSConfig,
cacheTimeout: const Duration(minutes: 30), // 30
);
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
);
final client = YxOSSClient(config);
try {
await client.initialize();
final fileData = Uint8List.fromList('Dynamic config upload test'.codeUnits);
final result = await client.uploadBytes(
fileData,
'dynamic-config-test.txt',
options: UploadOptions(
callbacks: UploadCallbacks(
onProgress: (sent, total) {
print('动态配置上传进度: ${(sent / total * 100).toStringAsFixed(1)}%');
},
),
),
);
print('动态配置上传成功: ${result.url}');
} catch (e) {
print('动态配置上传失败: $e');
} finally {
client.dispose();
}
}
/// 5:
/// :
Future<void> example5MultipartUpload() async {
print('\n=== 示例5: 分片上传 ===');
const authProvider = StaticAuthProvider(
accessKeyId: 'your_access_key_id',
accessKeySecret: 'your_access_key_secret',
);
const configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: 'large-files',
);
const config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
);
final client = YxOSSClient(config);
try {
await client.initialize();
// 200MB
final largeFileData = Uint8List(200 * 1024 * 1024);
for (int i = 0; i < largeFileData.length; i++) {
largeFileData[i] = i % 256;
}
print(
'准备上传 ${YxOSSFileUtils.formatFileSize(largeFileData.length)} 的大文件...');
final options = UploadOptions(
//
enableMultipart: true,
// 10MB
partSize: 10 * 1024 * 1024,
// 3
concurrency: 3,
// 100MB以上的文件使用分片上传
multipartThreshold: 100 * 1024 * 1024,
callbacks: UploadCallbacks(
onStart: () => print('开始分片上传...'),
onProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
final speed = YxOSSFileUtils.formatFileSize(sent ~/ 10); // 10
print('分片上传进度: $progress% - 速度: $speed');
},
onSuccess: (result) {
if (result is MultipartUploadResult) {
print('分片上传成功:');
print(' URL: ${result.url}');
print(' 分片数量: ${result.partCount}');
print(' 上传ID: ${result.uploadId}');
print(' 总耗时: ${result.uploadDuration} 毫秒');
}
},
onError: (error) => print('分片上传失败: $error'),
onComplete: () => print('分片上传完成'),
),
);
final result = await client.uploadBytes(
largeFileData,
'large-file-${DateTime.now().millisecondsSinceEpoch}.dat',
options: options,
);
print('大文件上传结果: ${result.url}');
} catch (e) {
print('大文件上传失败: $e');
} finally {
client.dispose();
}
}
/// 6:
Future<void> example6ErrorHandling() async {
print('\n=== 示例6: 错误处理和重试 ===');
const authProvider = StaticAuthProvider(
accessKeyId: 'invalid_key', // 使key来演示错误处理
accessKeySecret: 'invalid_secret',
);
const configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
);
const config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
//
retryConfig: RetryConfig(
maxRetries: 3,
initialInterval: 1000,
backoffMultiplier: 2.0,
enableExponentialBackoff: true,
),
);
final client = YxOSSClient(config);
try {
await client.initialize();
final fileData = Uint8List.fromList('Error handling test'.codeUnits);
await client.uploadBytes(
fileData,
'error-test.txt',
options: UploadOptions(
callbacks: UploadCallbacks(
onError: (error) {
print('捕获到错误:');
print(' 类型: ${error.type}');
print(' 消息: ${error.message}');
print(' 状态码: ${error.statusCode ?? 'N/A'}');
print(' 请求ID: ${error.requestId ?? 'N/A'}');
print(' 是否可重试: ${error.isRetryable}');
},
),
),
);
} catch (e) {
if (e is OSSError) {
print('OSS错误详情:');
print(' 错误类型: ${e.type}');
print(' 错误代码: ${e.code}');
print(' 错误消息: ${e.message}');
print(' HTTP状态码: ${e.statusCode}');
print(' 是否为认证错误: ${e.isAuthenticationError}');
print(' 是否为网络错误: ${e.isNetworkError}');
} else {
print('未知错误: $e');
}
} finally {
client.dispose();
}
}
/// 7:
class ProjectOSSAdapter {
late final YxOSSClient _client;
///
Future<void> initialize({
required String endpoint,
required String bucketName,
required String accessKeyId,
required String accessKeySecret,
String? customDomain,
}) async {
final authProvider = StaticAuthProvider(
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
);
final configProvider = StaticConfigProvider(
endpoint: endpoint,
bucketName: bucketName,
directory: 'app-uploads',
domain: customDomain,
);
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
logConfig: const LogConfig(
enabled: true,
level: LogLevel.warning, //
),
);
_client = YxOSSClient(config);
await _client.initialize();
}
///
Future<String> uploadImage(
Uint8List imageData,
String fileName, {
Function(double progress)? onProgress,
}) async {
final result = await _client.uploadBytes(
imageData,
'images/${YxOSSFileUtils.generateUniqueFileName(originalFileName: fileName)}',
options: UploadOptions(
contentType: YxOSSFileUtils.guessMimeType(fileName),
acl: ACLMode.publicRead,
callbacks: UploadCallbacks(
onProgress: (sent, total) {
onProgress?.call(sent / total);
},
),
),
);
return result.url;
}
///
Future<String> uploadDocument(
Uint8List documentData,
String fileName, {
Function(double progress)? onProgress,
}) async {
final result = await _client.uploadBytes(
documentData,
'documents/${YxOSSFileUtils.generateUniqueFileName(originalFileName: fileName)}',
options: UploadOptions(
contentType: YxOSSFileUtils.guessMimeType(fileName),
acl: ACLMode.private, //
callbacks: UploadCallbacks(
onProgress: (sent, total) {
onProgress?.call(sent / total);
},
),
),
);
return result.url;
}
///
void dispose() {
_client.dispose();
}
}

View File

@ -0,0 +1,69 @@
import 'dart:typed_data';
import 'package:yx_oss/yx_oss.dart';
/// 使
///
void main() async {
print('YX OSS 简单示例');
//
const authProvider = StaticAuthProvider(
accessKeyId: 'your_access_key_id',
accessKeySecret: 'your_access_key_secret',
);
//
const configProvider = StaticConfigProvider(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'your-bucket-name',
directory: 'uploads',
);
// OSS客户端配置
const config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
);
// OSS客户端
final client = YxOSSClient(config);
try {
//
await client.initialize();
//
final fileData = Uint8List.fromList('Hello, YX OSS!'.codeUnits);
//
final options = UploadOptions(
overwrite: true,
contentType: 'text/plain',
callbacks: UploadCallbacks(
onStart: () => print('开始上传...'),
onProgress: (sent, total) {
final progress = (sent / total * 100).toStringAsFixed(1);
print('上传进度: $progress% ($sent/$total)');
},
onSuccess: (result) => print('上传成功'),
onError: (error) => print('上传失败'),
onComplete: () => print('上传完成'),
),
);
//
final result = await client.uploadBytes(
fileData,
'simple-test.txt',
options: options,
);
print('上传成功: ${result.url}');
} catch (e) {
print('操作失败: $e');
} finally {
//
client.dispose();
}
}

View File

@ -0,0 +1,861 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import '../core/yx_oss_client.dart';
import '../core/yx_oss_config.dart';
import '../interfaces/auth_provider.dart';
import '../interfaces/config_provider.dart';
import '../interfaces/upload_callbacks.dart';
import '../models/upload_options.dart';
import '../utils/file_utils.dart';
import '../utils/oss_logger.dart';
// API相关类型定义
/// Server API的OSS配置结果
class YxOSSConfigModel {
final String? accessKeyId;
final String? accessKeySecret;
final String? endpoint;
final String? bucketName;
final int size;
const YxOSSConfigModel({
this.accessKeyId,
this.accessKeySecret,
this.endpoint,
this.bucketName,
required this.size,
});
factory YxOSSConfigModel.fromJson(Map<String, dynamic> json) {
return YxOSSConfigModel(
accessKeyId: json['accessKeyId'] as String?,
accessKeySecret: json['accessKeySecret'] as String?,
endpoint: json['endpoint'] as String?,
bucketName: json['bucketName'] as String?,
size: json['size'] as int? ?? 0,
);
}
}
/// Server API的OSS签名结果
class YxOSSSignModel {
final String? filePath;
final int fileSize;
final String? uploadUrl;
const YxOSSSignModel({
this.filePath,
required this.fileSize,
this.uploadUrl,
});
factory YxOSSSignModel.fromJson(Map<String, dynamic> json) {
return YxOSSSignModel(
filePath: json['filePath'] as String?,
fileSize: json['fileSize'] as int? ?? 0,
uploadUrl: json['uploadUrl'] as String?,
);
}
}
/// YX OSS API接口
abstract class YxOssServerInterface {
/// OSS配置
Future<YxOSSConfigModel> getOssConfig();
/// OSS预签名
Future<YxOSSSignModel> getOssSign({String? objectName, int? type});
/// OSS文件
Future<bool> deleteOSSFile({required String filePath});
}
///
///
/// YX OSS库集成到现有项目中
class ProjectOSSAdapter {
late final YxOSSClient _client;
bool _isInitialized = false;
///
///
/// [endpoint] OSS端点
/// [bucketName]
/// [accessKeyId] 访ID
/// [accessKeySecret] 访
/// [customDomain]
/// [directory] 'app-uploads'
Future<void> initialize({
required String endpoint,
required String bucketName,
required String accessKeyId,
required String accessKeySecret,
String? customDomain,
String directory = 'app-uploads',
}) async {
if (_isInitialized) {
return;
}
final authProvider = StaticAuthProvider(
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
);
final configProvider = StaticConfigProvider(
endpoint: endpoint,
bucketName: bucketName,
directory: directory,
domain: customDomain,
);
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
logConfig: const LogConfig(
enabled: true,
level: LogLevel.warning, //
logError: true,
),
);
_client = YxOSSClient(config);
await _client.initialize();
_isInitialized = true;
}
/// 使STS凭证初始化
///
/// [endpoint] OSS端点
/// [bucketName]
/// [stsCredentials] STS凭证
/// [credentialsRefresher]
/// [customDomain]
/// [directory]
Future<void> initializeWithSTS({
required String endpoint,
required String bucketName,
required STSCredentials stsCredentials,
required Future<STSCredentials> Function() credentialsRefresher,
String? customDomain,
String directory = 'app-uploads',
}) async {
if (_isInitialized) {
throw StateError('Adapter already initialized');
}
final authProvider = STSAuthProvider(
accessKeyId: stsCredentials.accessKeyId,
accessKeySecret: stsCredentials.accessKeySecret,
securityToken: stsCredentials.securityToken,
expiration: stsCredentials.expiration,
credentialsGetter: credentialsRefresher,
);
final configProvider = StaticConfigProvider(
endpoint: endpoint,
bucketName: bucketName,
directory: directory,
domain: customDomain,
);
final config = YxOSSConfig(
authProvider: authProvider,
configProvider: configProvider,
);
_client = YxOSSClient(config);
await _client.initialize();
_isInitialized = true;
}
///
///
/// [imageData]
/// [fileName]
/// [onProgress]
/// [quality] 0.0-1.0
Future<String> uploadImage(
Uint8List imageData,
String fileName, {
Function(double progress)? onProgress,
}) async {
_checkInitialized();
//
final uniqueFileName = YxOSSFileUtils.generateUniqueFileName(
originalFileName: fileName,
prefix: 'img_',
);
final result = await _client.uploadBytes(
imageData,
'images/$uniqueFileName',
options: UploadOptions(
contentType: YxOSSFileUtils.guessMimeType(fileName) ?? 'image/jpeg',
acl: ACLMode.publicRead,
metadata: {
'original_name': fileName,
'upload_time': DateTime.now().toIso8601String(),
'file_type': 'image',
},
callbacks: UploadCallbacks(
onProgress: (sent, total) {
onProgress?.call(sent / total);
},
),
),
);
return result.url;
}
///
///
/// [documentData]
/// [fileName]
/// [onProgress]
/// [isPublic] 访
Future<String> uploadDocument(
Uint8List documentData,
String fileName, {
Function(double progress)? onProgress,
bool isPublic = false,
}) async {
_checkInitialized();
final uniqueFileName = YxOSSFileUtils.generateUniqueFileName(
originalFileName: fileName,
prefix: 'doc_',
);
final result = await _client.uploadBytes(
documentData,
'documents/$uniqueFileName',
options: UploadOptions(
contentType: YxOSSFileUtils.guessMimeType(fileName) ??
'application/octet-stream',
acl: isPublic ? ACLMode.publicRead : ACLMode.private,
metadata: {
'original_name': fileName,
'upload_time': DateTime.now().toIso8601String(),
'file_type': 'document',
},
callbacks: UploadCallbacks(
onProgress: (sent, total) {
onProgress?.call(sent / total);
},
),
),
);
return result.url;
}
///
///
/// [avatarData]
/// [userId] ID
/// [onProgress]
Future<String> uploadUserAvatar(
Uint8List avatarData,
String userId, {
Function(double progress)? onProgress,
}) async {
_checkInitialized();
final fileName =
'avatar_${userId}_${DateTime.now().millisecondsSinceEpoch}.jpg';
final result = await _client.uploadBytes(
avatarData,
'avatars/$fileName',
options: UploadOptions(
contentType: 'image/jpeg',
acl: ACLMode.publicRead,
metadata: {
'user_id': userId,
'upload_time': DateTime.now().toIso8601String(),
'file_type': 'avatar',
},
callbacks: UploadCallbacks(
onProgress: (sent, total) {
onProgress?.call(sent / total);
},
),
),
);
return result.url;
}
/// 使
///
/// [fileData]
/// [fileName]
/// [onProgress]
/// [category]
Future<String> uploadLargeFile(
Uint8List fileData,
String fileName, {
Function(double progress)? onProgress,
String category = 'files',
}) async {
_checkInitialized();
final uniqueFileName = YxOSSFileUtils.generateUniqueFileName(
originalFileName: fileName,
prefix: 'large_',
);
final result = await _client.uploadBytes(
fileData,
'$category/$uniqueFileName',
options: UploadOptions(
contentType: YxOSSFileUtils.guessMimeType(fileName) ??
'application/octet-stream',
acl: ACLMode.private,
//
enableMultipart: true,
partSize: 10 * 1024 * 1024, // 10MB分片
multipartThreshold: 50 * 1024 * 1024, // 50MB阈值
concurrency: 3, // 3
metadata: {
'original_name': fileName,
'upload_time': DateTime.now().toIso8601String(),
'file_type': 'large_file',
'category': category,
},
callbacks: UploadCallbacks(
onProgress: (sent, total) {
onProgress?.call(sent / total);
},
),
),
);
return result.url;
}
///
///
/// [files] (, )
/// [onProgress]
/// [onSingleProgress]
/// [category]
Future<List<String>> uploadMultipleFiles(
List<(Uint8List, String)> files, {
Function(double progress)? onProgress,
Function(int fileIndex, double progress)? onSingleProgress,
String category = 'batch',
}) async {
_checkInitialized();
final results = <String>[];
var completedFiles = 0;
for (int i = 0; i < files.length; i++) {
final (fileData, fileName) = files[i];
try {
final url = await uploadDocument(
fileData,
fileName,
onProgress: (progress) {
onSingleProgress?.call(i, progress);
},
);
results.add(url);
completedFiles++;
//
onProgress?.call(completedFiles / files.length);
} catch (e) {
//
results.add('');
completedFiles++;
onProgress?.call(completedFiles / files.length);
}
}
return results;
}
///
void _checkInitialized() {
if (!_isInitialized) {
throw StateError('Adapter not initialized. Call initialize() first.');
}
}
///
void dispose() {
if (_isInitialized) {
_client.dispose();
_isInitialized = false;
}
}
///
bool get isInitialized => _isInitialized;
}
/// OSS配置
class StaticProjectOSSConfig {
final String endpoint;
final String bucketName;
final String directory;
final String? domain;
const StaticProjectOSSConfig({
required this.endpoint,
required this.bucketName,
required this.directory,
this.domain,
});
}
///
///
/// OSS配置
class ProjectOSSConfigManager {
static const Map<String, StaticProjectOSSConfig> _configs = {
'development': StaticProjectOSSConfig(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'dev-bucket',
directory: 'dev-uploads',
),
'testing': StaticProjectOSSConfig(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'test-bucket',
directory: 'test-uploads',
),
'production': StaticProjectOSSConfig(
endpoint: 'oss-cn-hangzhou.aliyuncs.com',
bucketName: 'prod-bucket',
directory: 'uploads',
domain: 'https://cdn.yourapp.com',
),
};
///
static StaticProjectOSSConfig? getConfig(String environment) {
return _configs[environment];
}
///
///
///
static StaticProjectOSSConfig getCurrentConfig() {
const environment =
String.fromEnvironment('ENV', defaultValue: 'development');
return _configs[environment] ?? _configs['development']!;
}
}
// ==================== MyInfo API的集成提供者 ====================
/// MyInfo API的动态配置提供者
class MyInfoConfigProvider implements ConfigProvider {
final YxOssServerInterface _api;
YxOSSConfigModel? _cachedProjectConfig;
DateTime? _cacheTime;
///
final int cacheValidityMinutes;
MyInfoConfigProvider(this._api, {this.cacheValidityMinutes = 30});
///
bool get _isCacheValid {
if (_cachedProjectConfig == null || _cacheTime == null) return false;
final now = DateTime.now();
return now.difference(_cacheTime!).inMinutes < cacheValidityMinutes;
}
///
Future<YxOSSConfigModel> _getConfig() async {
if (_isCacheValid) {
return _cachedProjectConfig!;
}
final config = await _api.getOssConfig();
_cachedProjectConfig = config;
_cacheTime = DateTime.now();
return config;
}
@override
Future<String> getEndpoint() async {
final config = await _getConfig();
return config.endpoint ?? 'oss-cn-hangzhou.aliyuncs.com';
}
@override
Future<String> getBucketName() async {
final config = await _getConfig();
return config.bucketName ?? 'default-bucket';
}
@override
Future<String> getDirectory() async {
// MyInfo API没有提供目录配置使
return 'app-uploads';
}
@override
Future<String?> getDomain() async {
// MyInfo API没有提供自定义域名配置
return null;
}
@override
Future<bool> isValid() async {
try {
final config = await _getConfig();
return config.endpoint != null &&
config.bucketName != null &&
config.endpoint!.isNotEmpty &&
config.bucketName!.isNotEmpty;
} catch (e) {
return false;
}
}
@override
Future<void> refresh() async {
clearCache();
await _getConfig(); //
}
///
void clearCache() {
_cachedProjectConfig = null;
_cacheTime = null;
}
///
Future<int> getFileSizeLimit() async {
final config = await _getConfig();
return config.size;
}
}
/// MyInfo API的动态认证提供者
class MyInfoAuthProvider implements AuthProvider {
final MyInfoConfigProvider _configProvider;
MyInfoAuthProvider(this._configProvider);
@override
Future<String> getAccessKeyId() async {
final config = await _configProvider._getConfig();
return config.accessKeyId ?? '';
}
@override
Future<String> getAccessKeySecret() async {
final config = await _configProvider._getConfig();
return config.accessKeySecret ?? '';
}
@override
Future<String?> getSecurityToken() async {
// MyInfo API目前不支持STS Token
return null;
}
@override
Future<bool> isValid() async {
try {
final accessKeyId = await getAccessKeyId();
final accessKeySecret = await getAccessKeySecret();
return accessKeyId.isNotEmpty && accessKeySecret.isNotEmpty;
} catch (e) {
return false;
}
}
@override
Future<void> refresh() async {
//
_configProvider.clearCache();
}
@override
Future<DateTime?> getExpiration() async {
// MyInfo API的认证信息没有明确的过期时间null表示永不过期
return null;
}
}
/// OSS管理器
///
///
class ProjectOSSManager {
late final YxOSSClient _client;
final YxOssServerInterface _api;
late final MyInfoConfigProvider _configProvider;
late final MyInfoAuthProvider _authProvider;
bool _isInitialized = false;
ProjectOSSManager(this._api);
///
Future<void> initialize() async {
if (_isInitialized) return;
_configProvider = MyInfoConfigProvider(_api);
_authProvider = MyInfoAuthProvider(_configProvider);
final config = YxOSSConfig(
authProvider: _authProvider,
configProvider: _configProvider,
);
_client = YxOSSClient(config);
await _client.initialize();
_isInitialized = true;
}
///
void _ensureInitialized() {
if (!_isInitialized) {
throw StateError(
'ProjectOSSManager not initialized. Call initialize() first.');
}
}
///
///
/// [fileBytes]
/// [fileName]
/// [onProgress]
/// [enableResume] false
/// [type] 12
Future<String> uploadBytes(
Uint8List fileBytes,
String fileName, {
void Function(int sent, int total)? onProgress,
bool enableResume = false,
int type = 1,
}) async {
_ensureInitialized();
final objectKey = YxOSSFileUtils.generateUniqueFileName(
originalFileName: fileName,
prefix: 'upload_',
);
final options = UploadOptions(
enableResume: enableResume,
callbacks:
onProgress != null ? UploadCallbacks(onProgress: onProgress) : null,
);
final result =
await _client.uploadBytes(fileBytes, objectKey, options: options);
return result.url;
}
///
///
/// [filePath]
/// [onProgress]
/// [enableResume] URL不支持断点续传
/// [type]
///
/// 使URL上传方式
Future<String> uploadFile(
String filePath, {
void Function(int sent, int total)? onProgress,
bool enableResume = true, // 使
int type = 1,
}) async {
_ensureInitialized();
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('文件不存在: $filePath');
}
final fileSize = file.lengthSync();
final fileName = filePath.split('/').last;
final extension = YxOSSFileUtils.getFileExtension(fileName);
// 使URL上传
final logger = OssLogger(_client.config.logConfig);
logger.info('使用预签名URL上传');
logger.debug('文件大小: ${YxOSSFileUtils.formatFileSize(fileSize)}');
try {
// URL
final sign = await _api.getOssSign(objectName: extension, type: type);
// URL和最终文件路径
String uploadUrl;
String finalPath;
if (sign.uploadUrl != null && sign.uploadUrl!.isNotEmpty) {
// uploadUrl和filePath分开
uploadUrl = sign.uploadUrl!;
finalPath = sign.filePath ?? '';
} else if (sign.filePath != null && sign.filePath!.isNotEmpty) {
// filePath包含预签名URL
uploadUrl = sign.filePath!;
finalPath = sign.filePath!.split('?')[0]; //
} else {
throw Exception('获取预签名URL失败uploadUrl和filePath都为空');
}
//
logger.debug('预签名信息');
logger.debug('uploadUrl: $uploadUrl');
logger.debug('finalPath: $finalPath');
// 使URL上传
return await _uploadWithPreSignedUrl(
filePath,
uploadUrl,
finalPath,
onProgress: onProgress,
);
} catch (e) {
// 退
throw Exception('预签名URL上传失败: $e');
}
}
/// 使URL上传PUT
///
/// URL的限制
/// 1. PUT操作
/// 2.
/// 3. HTTP headers
/// 4. <1MB
Future<String> _uploadWithPreSignedUrl(
String filePath,
String presignedUrl,
String finalPath, {
void Function(int sent, int total)? onProgress,
}) async {
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('文件不存在: $filePath');
}
final dio = Dio();
final response = await dio.put(
presignedUrl,
data: file.openRead(),
options: Options(
headers: null, // URL不能添加额外headers
validateStatus: (status) => status != null && status < 400,
),
onSendProgress: onProgress,
);
if (response.statusCode == 200) {
return finalPath.isNotEmpty ? finalPath : presignedUrl.split('?')[0];
} else {
throw Exception('预签名上传失败: HTTP ${response.statusCode}');
}
}
/// 使Access Key签名
///
/// 使
///
///
/// 1. InitiateMultipartUpload -> UploadPart -> CompleteMultipartUpload
/// 2.
/// 3.
/// 4.
/// 5.
// ignore: unused_element
Future<String> _uploadWithDirectMethod(
String filePath, {
void Function(int sent, int total)? onProgress,
bool enableResume = false,
}) async {
final dataSource = UploadDataSource.fromFilePath(filePath);
final objectKey = YxOSSFileUtils.generateUniqueFileName(
originalFileName: dataSource.fileName,
prefix: 'upload_',
);
final options = UploadOptions(
enableResume: enableResume,
callbacks:
onProgress != null ? UploadCallbacks(onProgress: onProgress) : null,
);
final result =
await _client.uploadDataSource(dataSource, objectKey, options: options);
return result.url;
}
/// OSS文件
///
/// [fileUrl] URL
Future<bool> deleteFile(String fileUrl) async {
_ensureInitialized();
try {
return await _api.deleteOSSFile(filePath: fileUrl);
} catch (e) {
return false;
}
}
///
Future<List<dynamic>> getResumableTasks() async {
_ensureInitialized();
return await _client.getResumableTasks();
}
///
Future<String> resumeUpload(String taskId) async {
_ensureInitialized();
final result = await _client.resumeUpload(taskId);
return result.url;
}
///
void cancelUpload(String taskId) {
_ensureInitialized();
_client.cancelUpload(taskId);
}
///
Future<void> cleanupExpiredData({int maxAgeDays = 7}) async {
_ensureInitialized();
await _client.cleanupExpiredResumeData(maxAgeDays: maxAgeDays);
}
/// OSS客户端
YxOSSClient get client {
_ensureInitialized();
return _client;
}
///
void refreshConfig() {
if (_isInitialized) {
_configProvider.clearCache();
}
}
///
void dispose() {
if (_isInitialized) {
_client.dispose();
_isInitialized = false;
}
}
}

View File

@ -0,0 +1,984 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import '../interfaces/auth_provider.dart';
import '../interfaces/resume_storage.dart';
import '../models/oss_error.dart' as error_model;
import '../models/upload_options.dart';
import '../models/upload_result.dart' as result_model;
import '../utils/file_utils.dart';
import '../utils/http_utils.dart';
import '../utils/local_resume_storage.dart';
import '../utils/oss_logger.dart';
import 'yx_oss_config.dart';
import 'yx_oss_signer.dart';
/// YX OSS客户端
///
/// OSS文件上传
class YxOSSClient {
final YxOSSConfig _config;
late final Dio _dio;
final Map<String, CancelToken> _uploadTasks = {};
late final ResumeStorage _resumeStorage;
late final OssLogger _logger;
/// Expose read-only config for integration helpers
YxOSSConfig get config => _config;
YxOSSClient(this._config, {ResumeStorage? resumeStorage}) {
_dio = HttpUtils.createDio(
timeoutConfig: _config.timeoutConfig,
logConfig: _config.logConfig,
);
_resumeStorage = resumeStorage ?? LocalResumeStorage();
_logger = OssLogger(_config.logConfig);
}
///
///
Future<void> initialize() async {
if (!await _config.validate()) {
throw error_model.OSSError.configuration('Invalid OSS configuration');
}
}
///
///
/// [data]
/// [objectKey] OSS中的路径
/// [options]
Future<result_model.UploadResult> uploadBytes(
Uint8List data,
String objectKey, {
UploadOptions? options,
}) async {
final dataSource = BytesDataSource(
data,
YxOSSFileUtils.getFileNameWithoutExtension(objectKey),
mimeType: options?.contentType ?? YxOSSFileUtils.guessMimeType(objectKey),
);
return _uploadDataSource(
dataSource, objectKey, options ?? const UploadOptions());
}
///
///
/// [dataSource]
/// [objectKey]
/// [options]
Future<result_model.UploadResult> uploadDataSource(
UploadDataSource dataSource,
String objectKey, {
UploadOptions? options,
}) async {
return _uploadDataSource(
dataSource, objectKey, options ?? const UploadOptions());
}
///
Future<result_model.UploadResult> _uploadDataSource(
UploadDataSource dataSource,
String objectKey,
UploadOptions options,
) async {
//
if (!options.validate()) {
throw error_model.OSSError.configuration('Invalid upload options');
}
//
if (!YxOSSFileUtils.isValidObjectKey(objectKey)) {
throw error_model.OSSError.configuration(
'Invalid object key: $objectKey');
}
//
options.callbacks?.onStart?.call();
final stopwatch = Stopwatch()..start();
try {
result_model.UploadResult result;
//
if (options.enableMultipart &&
dataSource.size > options.multipartThreshold) {
result = await _uploadMultipart(dataSource, objectKey, options);
} else {
result = await _uploadSingle(dataSource, objectKey, options);
}
stopwatch.stop();
//
final finalResult = result_model.UploadResult.fromResponse(
url: result.url,
objectKey: result.objectKey,
fileSize: result.fileSize,
uploadDuration: stopwatch.elapsedMilliseconds,
headers: {'etag': result.etag},
mimeType: result.mimeType,
md5Hash: result.md5Hash,
metadata: result.metadata,
);
//
options.callbacks?.onSuccess?.call(finalResult);
return finalResult;
} catch (e) {
stopwatch.stop();
final error = e is error_model.OSSError
? e
: error_model.OSSError.unknown('Upload failed: $e',
originalException: e);
//
options.callbacks?.onError?.call(error);
rethrow;
} finally {
//
options.callbacks?.onComplete?.call();
}
}
///
Future<result_model.UploadResult> _uploadSingle(
UploadDataSource dataSource,
String objectKey,
UploadOptions options,
) async {
final taskId = _generateTaskId(objectKey, dataSource.size);
final cancelToken = CancelToken();
_uploadTasks[taskId] = cancelToken;
try {
//
final endpoint = await _config.configProvider.getEndpoint();
final bucket = await _config.configProvider.getBucketName();
final directory = await _config.configProvider.getDirectory();
final domain = await _config.configProvider.getDomain();
//
final fullObjectKey =
directory.isEmpty ? objectKey : '$directory/$objectKey';
// 使URL模式
if (_config.authProvider is PreSignedAuthProvider) {
return await _uploadWithPreSignedUrl(
dataSource,
fullObjectKey,
options,
cancelToken,
);
}
//
return await _uploadWithSignature(
dataSource,
fullObjectKey,
endpoint,
bucket,
domain,
options,
cancelToken,
);
} finally {
_uploadTasks.remove(taskId);
}
}
/// 使URL上传
Future<result_model.UploadResult> _uploadWithPreSignedUrl(
UploadDataSource dataSource,
String objectKey,
UploadOptions options,
CancelToken cancelToken,
) async {
final authProvider = _config.authProvider as PreSignedAuthProvider;
final preSignedUrl =
await authProvider.getPreSignedUrl(dataSource.fileName);
final data = await dataSource.readAsBytes();
final response = await _dio.put(
preSignedUrl,
data: Stream.fromIterable([data]),
cancelToken: cancelToken,
options: Options(
headers: {
'Content-Type': dataSource.mimeType ?? 'application/octet-stream',
'Content-Length': data.length.toString(),
},
),
onSendProgress: (sent, total) {
options.callbacks?.onProgress?.call(sent, total);
},
);
if (!HttpUtils.isSuccessStatusCode(response.statusCode)) {
throw error_model.OSSError.fromResponse(
response.statusCode!,
response.data?.toString(),
requestId: HttpUtils.extractRequestId(response),
);
}
// URL中提取实际的OSS URL
final uri = Uri.parse(preSignedUrl);
final ossUrl = '${uri.scheme}://${uri.host}${uri.path}';
return result_model.UploadResult.fromResponse(
url: ossUrl,
objectKey: objectKey,
fileSize: data.length,
uploadDuration: 0, //
headers: response.headers.map,
mimeType: dataSource.mimeType,
);
}
/// 使
Future<result_model.UploadResult> _uploadWithSignature(
UploadDataSource dataSource,
String objectKey,
String endpoint,
String bucket,
String? domain,
UploadOptions options,
CancelToken cancelToken,
) async {
final data = await dataSource.readAsBytes();
final contentMD5 = YxOSSSigner.calculateMD5(data);
final currentTime = DateTime.now();
final httpDate = YxOSSSigner.formatHttpDate(currentTime);
//
final headers = <String, String>{
'Content-Type': dataSource.mimeType ?? 'application/octet-stream',
'Content-Length': data.length.toString(),
'Content-MD5': contentMD5,
'Date': httpDate,
'Host': '$bucket.$endpoint',
};
//
if (options.headers != null) {
headers.addAll(options.headers!);
}
// OSS特定头部
if (options.acl != null) {
headers['x-oss-object-acl'] = options.acl!.value;
}
if (options.storageClass != null) {
headers['x-oss-storage-class'] = options.storageClass!.value;
}
if (options.metadata != null) {
for (final entry in options.metadata!.entries) {
headers['x-oss-meta-${entry.key}'] = entry.value;
}
}
//
final accessKeyId = await _config.authProvider.getAccessKeyId();
final accessKeySecret = await _config.authProvider.getAccessKeySecret();
final securityToken = await _config.authProvider.getSecurityToken();
//
if (securityToken != null) {
headers['x-oss-security-token'] = securityToken;
}
//
final signature = YxOSSSigner.generateSignature(
method: 'PUT',
objectKey: objectKey,
bucket: bucket,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
headers: headers,
securityToken: securityToken,
);
headers['Authorization'] = signature;
// URL
final uploadUrl = 'https://$bucket.$endpoint/$objectKey';
//
final response = await _dio.put(
uploadUrl,
data: Stream.fromIterable([data]),
cancelToken: cancelToken,
options: Options(headers: headers),
onSendProgress: (sent, total) {
options.callbacks?.onProgress?.call(sent, total);
},
);
if (!HttpUtils.isSuccessStatusCode(response.statusCode)) {
throw error_model.OSSError.fromResponse(
response.statusCode!,
response.data?.toString(),
requestId: HttpUtils.extractRequestId(response),
);
}
// 访URL
final accessUrl = domain != null ? '$domain/$objectKey' : uploadUrl;
return result_model.UploadResult.fromResponse(
url: accessUrl,
objectKey: objectKey,
fileSize: data.length,
uploadDuration: 0, //
headers: response.headers.map,
mimeType: dataSource.mimeType,
md5Hash: contentMD5,
);
}
///
Future<result_model.UploadResult> _uploadMultipart(
UploadDataSource dataSource,
String objectKey,
UploadOptions options,
) async {
final taskId = _generateTaskId(objectKey, dataSource.size);
// 1.
result_model.ResumeUploadInfo? resumeInfo;
if (options.enableResume) {
resumeInfo = await _loadResumeInfo(taskId);
}
String uploadId;
List<result_model.PartInfo> completedParts = [];
if (resumeInfo != null) {
// 使ID和已完成的分片
uploadId = resumeInfo.uploadId;
completedParts = resumeInfo.completedParts;
//
options.onStart?.call(taskId, resumeInfo.uploadedSize, dataSource.size);
} else {
//
uploadId = await _initiateMultipartUpload(dataSource, objectKey, options);
//
if (options.enableResume) {
resumeInfo = result_model.ResumeUploadInfo(
taskId: taskId,
filePath: dataSource.filePath ?? '',
objectKey: objectKey,
uploadId: uploadId,
totalSize: dataSource.size,
partSize: options.partSize,
completedParts: [],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
uploadOptions: options.toJson(),
);
await _saveResumeInfo(taskId, resumeInfo);
}
//
options.onStart?.call(taskId, 0, dataSource.size);
}
try {
// 2.
final partRanges =
YxOSSFileUtils.calculatePartRanges(dataSource.size, options.partSize);
// 3.
final pendingRanges = partRanges.where((range) {
return !completedParts
.any((part) => part.partNumber == range.partNumber);
}).toList();
// 4.
if (pendingRanges.isNotEmpty) {
final newParts = await _uploadParts(
dataSource,
objectKey,
uploadId,
pendingRanges,
options,
existingParts: completedParts,
taskId: taskId,
resumeInfo: resumeInfo,
);
completedParts.addAll(newParts);
}
// 5.
final result = await _completeMultipartUpload(
dataSource, objectKey, uploadId, completedParts, options);
// 6.
if (options.enableResume) {
await _deleteResumeInfo(taskId);
}
return result;
} catch (e) {
//
if (!options.enableResume) {
await _abortMultipartUpload(objectKey, uploadId);
}
rethrow;
}
}
///
Future<String> _initiateMultipartUpload(
UploadDataSource dataSource,
String objectKey,
UploadOptions options,
) async {
final endpoint = await _config.configProvider.getEndpoint();
final bucket = await _config.configProvider.getBucketName();
final url = HttpUtils.buildMultipartUploadUrl(
endpoint: endpoint,
bucket: bucket,
objectKey: objectKey,
);
final headers = <String, String>{
'Content-Type': dataSource.mimeType ?? 'application/octet-stream',
'Date': YxOSSSigner.formatHttpDate(DateTime.now()),
'Host': '$bucket.$endpoint',
};
// OSS特定头部
if (options.acl != null) {
headers['x-oss-object-acl'] = options.acl!.value;
}
if (options.storageClass != null) {
headers['x-oss-storage-class'] = options.storageClass!.value;
}
//
final accessKeyId = await _config.authProvider.getAccessKeyId();
final accessKeySecret = await _config.authProvider.getAccessKeySecret();
final securityToken = await _config.authProvider.getSecurityToken();
if (securityToken != null) {
headers['x-oss-security-token'] = securityToken;
}
final signature = YxOSSSigner.generateSignature(
method: 'POST',
objectKey: '$objectKey?uploads',
bucket: bucket,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
headers: headers,
securityToken: securityToken,
);
headers['Authorization'] = signature;
final response = await _dio.post(
url,
options: Options(
headers: headers,
responseType: ResponseType.plain,
),
);
if (!HttpUtils.isSuccessStatusCode(response.statusCode)) {
throw error_model.OSSError.fromResponse(
response.statusCode!,
response.data?.toString(),
requestId: HttpUtils.extractRequestId(response),
);
}
// uploadId
final responseBody = response.data as String;
final uploadIdMatch =
RegExp(r'<UploadId>([^<]+)</UploadId>').firstMatch(responseBody);
if (uploadIdMatch == null) {
throw error_model.OSSError.server(
'Failed to extract upload ID from response');
}
return uploadIdMatch.group(1)!;
}
///
Future<List<result_model.PartInfo>> _uploadParts(
UploadDataSource dataSource,
String objectKey,
String uploadId,
List<PartRange> partRanges,
UploadOptions options, {
List<result_model.PartInfo> existingParts = const [],
String? taskId,
result_model.ResumeUploadInfo? resumeInfo,
}) async {
final parts = <result_model.PartInfo>[];
final semaphore = Semaphore(options.concurrency);
final futures = <Future<result_model.PartInfo>>[];
//
int totalSent = existingParts.fold<int>(0, (sum, part) => sum + part.size);
for (final partRange in partRanges) {
final future = semaphore.acquire().then((_) async {
try {
final partInfo = await _uploadPart(
dataSource,
objectKey,
uploadId,
partRange,
options,
(sent, total) {
final currentTotalSent = totalSent + sent;
options.callbacks?.onProgress
?.call(currentTotalSent, dataSource.size);
},
);
//
if (options.enableResume && taskId != null && resumeInfo != null) {
final updatedResumeInfo = resumeInfo.addCompletedPart(partInfo);
await _saveResumeInfo(taskId, updatedResumeInfo);
}
totalSent += partInfo.size;
return partInfo;
} finally {
semaphore.release();
}
});
futures.add(future);
}
final results = await Future.wait(futures);
parts.addAll(results);
//
parts.sort((a, b) => a.partNumber.compareTo(b.partNumber));
return parts;
}
///
Future<result_model.PartInfo> _uploadPart(
UploadDataSource dataSource,
String objectKey,
String uploadId,
PartRange partRange,
UploadOptions options,
void Function(int sent, int total)? onProgress,
) async {
final stopwatch = Stopwatch()..start();
final endpoint = await _config.configProvider.getEndpoint();
final bucket = await _config.configProvider.getBucketName();
final url = HttpUtils.buildMultipartUploadUrl(
endpoint: endpoint,
bucket: bucket,
objectKey: objectKey,
uploadId: uploadId,
partNumber: partRange.partNumber,
);
//
final partData = Uint8List(partRange.size);
await for (final chunk
in dataSource.openRead(partRange.start, partRange.end + 1)) {
partData.setAll(0, chunk);
break; //
}
final headers = <String, String>{
'Content-Length': partRange.size.toString(),
'Date': YxOSSSigner.formatHttpDate(DateTime.now()),
'Host': '$bucket.$endpoint',
};
//
final accessKeyId = await _config.authProvider.getAccessKeyId();
final accessKeySecret = await _config.authProvider.getAccessKeySecret();
final securityToken = await _config.authProvider.getSecurityToken();
if (securityToken != null) {
headers['x-oss-security-token'] = securityToken;
}
final signature = YxOSSSigner.generateSignature(
method: 'PUT',
objectKey:
'$objectKey?partNumber=${partRange.partNumber}&uploadId=$uploadId',
bucket: bucket,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
headers: headers,
securityToken: securityToken,
);
headers['Authorization'] = signature;
final response = await _dio.put(
url,
data: Stream.fromIterable([partData]),
options: Options(headers: headers),
onSendProgress: onProgress,
);
if (!HttpUtils.isSuccessStatusCode(response.statusCode)) {
throw error_model.OSSError.fromResponse(
response.statusCode!,
response.data?.toString(),
requestId: HttpUtils.extractRequestId(response),
);
}
stopwatch.stop();
final etag = HttpUtils.extractETag(response);
if (etag == null) {
throw error_model.OSSError.server(
'Failed to extract ETag from part upload response');
}
return result_model.PartInfo(
partNumber: partRange.partNumber,
etag: etag,
size: partRange.size,
uploadDuration: stopwatch.elapsedMilliseconds,
);
}
///
Future<result_model.MultipartUploadResult> _completeMultipartUpload(
UploadDataSource dataSource,
String objectKey,
String uploadId,
List<result_model.PartInfo> parts,
UploadOptions options,
) async {
final endpoint = await _config.configProvider.getEndpoint();
final bucket = await _config.configProvider.getBucketName();
final domain = await _config.configProvider.getDomain();
final url = HttpUtils.buildMultipartUploadUrl(
endpoint: endpoint,
bucket: bucket,
objectKey: objectKey,
uploadId: uploadId,
);
// XML
final buffer = StringBuffer();
buffer.writeln('<CompleteMultipartUpload>');
for (final part in parts) {
buffer.writeln('<Part>');
buffer.writeln('<PartNumber>${part.partNumber}</PartNumber>');
buffer.writeln('<ETag>${part.etag}</ETag>');
buffer.writeln('</Part>');
}
buffer.writeln('</CompleteMultipartUpload>');
final xmlData = buffer.toString();
final xmlBytes = Uint8List.fromList(utf8.encode(xmlData));
final contentMD5 = YxOSSSigner.calculateMD5(xmlBytes);
final headers = <String, String>{
'Content-Type': 'application/xml',
'Content-Length': xmlBytes.length.toString(),
'Content-MD5': contentMD5,
'Date': YxOSSSigner.formatHttpDate(DateTime.now()),
'Host': '$bucket.$endpoint',
};
//
final accessKeyId = await _config.authProvider.getAccessKeyId();
final accessKeySecret = await _config.authProvider.getAccessKeySecret();
final securityToken = await _config.authProvider.getSecurityToken();
if (securityToken != null) {
headers['x-oss-security-token'] = securityToken;
}
final signature = YxOSSSigner.generateSignature(
method: 'POST',
objectKey: '$objectKey?uploadId=$uploadId',
bucket: bucket,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
headers: headers,
securityToken: securityToken,
);
headers['Authorization'] = signature;
final response = await _dio.post(
url,
data: Stream.fromIterable([xmlBytes]),
options: Options(
headers: headers,
responseType: ResponseType.plain,
),
);
if (!HttpUtils.isSuccessStatusCode(response.statusCode)) {
throw error_model.OSSError.fromResponse(
response.statusCode!,
response.data?.toString(),
requestId: HttpUtils.extractRequestId(response),
);
}
// 访URL
final accessUrl = domain != null
? '$domain/$objectKey'
: 'https://$bucket.$endpoint/$objectKey';
final totalUploadDuration =
parts.fold<int>(0, (sum, part) => sum + part.uploadDuration);
return result_model.MultipartUploadResult(
url: accessUrl,
objectKey: objectKey,
fileSize: dataSource.size,
uploadDuration: totalUploadDuration,
uploadTime: DateTime.now(),
partCount: parts.length,
uploadId: uploadId,
parts: parts,
etag: HttpUtils.extractETag(response),
mimeType: dataSource.mimeType,
);
}
///
Future<void> _abortMultipartUpload(String objectKey, String uploadId) async {
try {
final endpoint = await _config.configProvider.getEndpoint();
final bucket = await _config.configProvider.getBucketName();
final url = HttpUtils.buildMultipartUploadUrl(
endpoint: endpoint,
bucket: bucket,
objectKey: objectKey,
uploadId: uploadId,
);
final headers = <String, String>{
'Date': YxOSSSigner.formatHttpDate(DateTime.now()),
'Host': '$bucket.$endpoint',
};
//
final accessKeyId = await _config.authProvider.getAccessKeyId();
final accessKeySecret = await _config.authProvider.getAccessKeySecret();
final securityToken = await _config.authProvider.getSecurityToken();
if (securityToken != null) {
headers['x-oss-security-token'] = securityToken;
}
final signature = YxOSSSigner.generateSignature(
method: 'DELETE',
objectKey: '$objectKey?uploadId=$uploadId',
bucket: bucket,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
headers: headers,
securityToken: securityToken,
);
headers['Authorization'] = signature;
await _dio.delete(
url,
options: Options(headers: headers),
);
} catch (e) {
//
if (_config.logConfig.enabled &&
_config.logConfig.level <= LogLevel.warning) {
_config.logConfig.customHandler?.call(
'Failed to abort multipart upload: $e',
LogLevel.warning,
);
}
}
}
///
void cancelUpload(String taskId) {
final cancelToken = _uploadTasks[taskId];
if (cancelToken != null && !cancelToken.isCancelled) {
cancelToken.cancel('Upload cancelled by user');
_uploadTasks.remove(taskId);
}
}
///
void cancelAllUploads() {
for (final cancelToken in _uploadTasks.values) {
if (!cancelToken.isCancelled) {
cancelToken.cancel('All uploads cancelled');
}
}
_uploadTasks.clear();
}
///
void dispose() {
cancelAllUploads();
_dio.close();
}
// ==================== ====================
/// ID
String _generateTaskId(String objectKey, int fileSize) {
return YxOSSFileUtils.generateUniqueFileName(
originalFileName: '$objectKey-$fileSize',
preserveExtension: false,
prefix: 'task_',
);
}
///
Future<void> _saveResumeInfo(
String taskId, result_model.ResumeUploadInfo resumeInfo) async {
try {
await _resumeStorage.saveResumeInfo(taskId, resumeInfo.toJson());
} catch (e) {
//
_logger.warn('Failed to save resume info for task $taskId: $e');
}
}
///
Future<result_model.ResumeUploadInfo?> _loadResumeInfo(String taskId) async {
try {
final data = await _resumeStorage.loadResumeInfo(taskId);
if (data != null) {
return result_model.ResumeUploadInfo.fromJson(data);
}
} catch (e) {
//
_logger.warn('Failed to load resume info for task $taskId: $e');
await _deleteResumeInfo(taskId);
}
return null;
}
///
Future<void> _deleteResumeInfo(String taskId) async {
try {
await _resumeStorage.deleteResumeInfo(taskId);
} catch (e) {
//
_logger.warn('Failed to delete resume info for task $taskId: $e');
}
}
///
Future<List<result_model.ResumeUploadInfo>> getResumableTasks() async {
try {
final taskIds = await _resumeStorage.getAllTaskIds();
final tasks = <result_model.ResumeUploadInfo>[];
for (final taskId in taskIds) {
final resumeInfo = await _loadResumeInfo(taskId);
if (resumeInfo != null) {
tasks.add(resumeInfo);
}
}
return tasks;
} catch (e) {
return [];
}
}
///
Future<result_model.UploadResult> resumeUpload(String taskId) async {
final resumeInfo = await _loadResumeInfo(taskId);
if (resumeInfo == null) {
throw error_model.OSSError.configuration(
'Resume data not found for task: $taskId');
}
// UploadDataSource
if (resumeInfo.filePath.isEmpty) {
throw error_model.OSSError.configuration(
'File path not available in resume data');
}
//
final dataSource = UploadDataSource.fromFilePath(resumeInfo.filePath);
// UploadOptions
final options = UploadOptions.fromJson(resumeInfo.uploadOptions);
//
return await _uploadMultipart(dataSource, resumeInfo.objectKey, options);
}
///
Future<void> clearAllResumeData() async {
await _resumeStorage.clearAll();
}
///
Future<void> cleanupExpiredResumeData({int maxAgeDays = 7}) async {
if (_resumeStorage is LocalResumeStorage) {
await (_resumeStorage as LocalResumeStorage)
.cleanupExpiredTasks(maxAgeDays);
}
}
}
///
class Semaphore {
final int maxCount;
int _currentCount;
final Queue<Completer<void>> _waitQueue = Queue<Completer<void>>();
Semaphore(this.maxCount) : _currentCount = maxCount;
Future<void> acquire() async {
if (_currentCount > 0) {
_currentCount--;
return;
}
final completer = Completer<void>();
_waitQueue.addLast(completer);
return completer.future;
}
void release() {
if (_waitQueue.isNotEmpty) {
final completer = _waitQueue.removeFirst();
completer.complete();
} else {
_currentCount++;
}
}
}

View File

@ -0,0 +1,284 @@
import '../interfaces/auth_provider.dart';
import '../interfaces/config_provider.dart';
/// YX OSS客户端配置
class YxOSSConfig {
///
final AuthProvider authProvider;
///
final ConfigProvider configProvider;
///
final TimeoutConfig timeoutConfig;
///
final RetryConfig retryConfig;
///
final LogConfig logConfig;
const YxOSSConfig({
required this.authProvider,
required this.configProvider,
this.timeoutConfig = const TimeoutConfig(),
this.retryConfig = const RetryConfig(),
this.logConfig = const LogConfig(),
});
///
Future<bool> validate() async {
try {
//
if (!await authProvider.isValid()) {
return false;
}
//
if (!await configProvider.isValid()) {
return false;
}
//
if (!timeoutConfig.validate()) {
return false;
}
//
if (!retryConfig.validate()) {
return false;
}
return true;
} catch (e) {
return false;
}
}
}
///
class TimeoutConfig {
///
final int connectTimeout;
///
final int sendTimeout;
///
final int receiveTimeout;
const TimeoutConfig({
this.connectTimeout = 60000, // 60
this.sendTimeout = 300000, // 5
this.receiveTimeout = 300000, // 5
});
bool validate() {
return connectTimeout > 0 && sendTimeout > 0 && receiveTimeout > 0;
}
TimeoutConfig copyWith({
int? connectTimeout,
int? sendTimeout,
int? receiveTimeout,
}) {
return TimeoutConfig(
connectTimeout: connectTimeout ?? this.connectTimeout,
sendTimeout: sendTimeout ?? this.sendTimeout,
receiveTimeout: receiveTimeout ?? this.receiveTimeout,
);
}
}
///
class RetryConfig {
///
final int maxRetries;
///
final int initialInterval;
///
final double backoffMultiplier;
///
final int maxInterval;
/// 退
final bool enableExponentialBackoff;
///
final Set<Type> retryableErrors;
const RetryConfig({
this.maxRetries = 3,
this.initialInterval = 1000,
this.backoffMultiplier = 2.0,
this.maxInterval = 30000,
this.enableExponentialBackoff = true,
this.retryableErrors = const {},
});
bool validate() {
return maxRetries >= 0 &&
initialInterval > 0 &&
backoffMultiplier > 1.0 &&
maxInterval >= initialInterval;
}
///
int calculateInterval(int retryAttempt) {
if (!enableExponentialBackoff) {
return initialInterval;
}
final interval = (initialInterval *
(retryAttempt == 0
? 1
: (1 << (retryAttempt - 1)) * backoffMultiplier))
.toInt();
return interval > maxInterval ? maxInterval : interval;
}
RetryConfig copyWith({
int? maxRetries,
int? initialInterval,
double? backoffMultiplier,
int? maxInterval,
bool? enableExponentialBackoff,
Set<Type>? retryableErrors,
}) {
return RetryConfig(
maxRetries: maxRetries ?? this.maxRetries,
initialInterval: initialInterval ?? this.initialInterval,
backoffMultiplier: backoffMultiplier ?? this.backoffMultiplier,
maxInterval: maxInterval ?? this.maxInterval,
enableExponentialBackoff:
enableExponentialBackoff ?? this.enableExponentialBackoff,
retryableErrors: retryableErrors ?? this.retryableErrors,
);
}
}
///
class LogConfig {
///
final bool enabled;
///
final LogLevel level;
///
final bool logRequest;
///
final bool logResponse;
///
final bool logError;
///
final void Function(String message, LogLevel level)? customHandler;
const LogConfig({
this.enabled = true,
this.level = LogLevel.info,
this.logRequest = false,
this.logResponse = false,
this.logError = true,
this.customHandler,
});
LogConfig copyWith({
bool? enabled,
LogLevel? level,
bool? logRequest,
bool? logResponse,
bool? logError,
void Function(String message, LogLevel level)? customHandler,
}) {
return LogConfig(
enabled: enabled ?? this.enabled,
level: level ?? this.level,
logRequest: logRequest ?? this.logRequest,
logResponse: logResponse ?? this.logResponse,
logError: logError ?? this.logError,
customHandler: customHandler ?? this.customHandler,
);
}
}
///
enum LogLevel {
debug(0),
info(1),
warning(2),
error(3);
const LogLevel(this.value);
final int value;
bool operator >=(LogLevel other) => value >= other.value;
bool operator <=(LogLevel other) => value <= other.value;
bool operator >(LogLevel other) => value > other.value;
bool operator <(LogLevel other) => value < other.value;
}
/// OSS区域配置
class RegionConfig {
///
final String region;
/// 访
final bool isInternal;
///
final String? customEndpoint;
const RegionConfig({
required this.region,
this.isInternal = false,
this.customEndpoint,
});
/// URL
String getEndpoint() {
if (customEndpoint != null) {
return customEndpoint!;
}
final prefix = isInternal ? 'oss-' : 'oss-';
final suffix = isInternal ? '-internal.aliyuncs.com' : '.aliyuncs.com';
return '$prefix$region$suffix';
}
}
///
class CommonRegions {
static const RegionConfig hangzhou = RegionConfig(region: 'cn-hangzhou');
static const RegionConfig beijing = RegionConfig(region: 'cn-beijing');
static const RegionConfig shanghai = RegionConfig(region: 'cn-shanghai');
static const RegionConfig shenzhen = RegionConfig(region: 'cn-shenzhen');
static const RegionConfig qingdao = RegionConfig(region: 'cn-qingdao');
static const RegionConfig zhangjiakou =
RegionConfig(region: 'cn-zhangjiakou');
static const RegionConfig huhehaote = RegionConfig(region: 'cn-huhehaote');
static const RegionConfig chengdu = RegionConfig(region: 'cn-chengdu');
static const RegionConfig hongkong = RegionConfig(region: 'cn-hongkong');
//
static const RegionConfig singapore = RegionConfig(region: 'ap-southeast-1');
static const RegionConfig sydney = RegionConfig(region: 'ap-southeast-2');
static const RegionConfig kualaLumpur =
RegionConfig(region: 'ap-southeast-3');
static const RegionConfig jakarta = RegionConfig(region: 'ap-southeast-5');
static const RegionConfig mumbai = RegionConfig(region: 'ap-south-1');
static const RegionConfig tokyo = RegionConfig(region: 'ap-northeast-1');
static const RegionConfig seoul = RegionConfig(region: 'ap-northeast-2');
static const RegionConfig virginia = RegionConfig(region: 'us-east-1');
static const RegionConfig oregon = RegionConfig(region: 'us-west-1');
static const RegionConfig london = RegionConfig(region: 'eu-west-1');
static const RegionConfig frankfurt = RegionConfig(region: 'eu-central-1');
static const RegionConfig dubai = RegionConfig(region: 'me-east-1');
}

View File

@ -0,0 +1,356 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
/// OSS签名算法实现
/// OSS官方签名算法
class YxOSSSigner {
/// OSS授权签名
///
/// [method] HTTP方法
/// [objectKey]
/// [bucket]
/// [accessKeyId] 访ID
/// [accessKeySecret] 访
/// [headers]
/// [securityToken] STS模式
static String generateSignature({
required String method,
required String objectKey,
required String bucket,
required String accessKeyId,
required String accessKeySecret,
required Map<String, String> headers,
String? securityToken,
}) {
// OSS头部
final canonicalizedOSSHeaders =
_buildCanonicalizedOSSHeaders(headers, securityToken);
//
final canonicalizedResource =
_buildCanonicalizedResource(bucket, objectKey);
//
final contentMD5 = headers['content-md5'] ?? headers['Content-MD5'] ?? '';
final contentType =
headers['content-type'] ?? headers['Content-Type'] ?? '';
final date = headers['date'] ?? headers['Date'] ?? '';
//
final stringToSign = _buildStringToSign(
method: method,
contentMD5: contentMD5,
contentType: contentType,
date: date,
canonicalizedOSSHeaders: canonicalizedOSSHeaders,
canonicalizedResource: canonicalizedResource,
);
// HMAC-SHA1签名
final signature = _calculateSignature(stringToSign, accessKeySecret);
return 'OSS $accessKeyId:$signature';
}
/// URL
///
/// [method] HTTP方法
/// [objectKey]
/// [bucket]
/// [endpoint] OSS端点
/// [accessKeyId] 访ID
/// [accessKeySecret] 访
/// [expires] Unix时间戳
/// [securityToken] STS模式
/// [params]
static String generatePreSignedUrl({
required String method,
required String objectKey,
required String bucket,
required String endpoint,
required String accessKeyId,
required String accessKeySecret,
required int expires,
String? securityToken,
Map<String, String>? params,
}) {
//
final queryParams = <String, String>{
'OSSAccessKeyId': accessKeyId,
'Expires': expires.toString(),
'Signature': '',
};
if (securityToken != null) {
queryParams['security-token'] = securityToken;
}
if (params != null) {
queryParams.addAll(params);
}
//
final canonicalizedResource = _buildCanonicalizedResourceWithParams(
bucket,
objectKey,
queryParams,
);
//
final stringToSign = _buildStringToSign(
method: method,
contentMD5: '',
contentType: '',
date: expires.toString(),
canonicalizedOSSHeaders: '',
canonicalizedResource: canonicalizedResource,
);
//
final signature = _calculateSignature(stringToSign, accessKeySecret);
queryParams['Signature'] = signature;
// URL
final queryString = queryParams.entries
.map((e) =>
'${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
.join('&');
return 'https://$bucket.$endpoint/$objectKey?$queryString';
}
/// MD5哈希值
static String calculateMD5(Uint8List data) {
final digest = md5.convert(data);
return base64Encode(digest.bytes);
}
/// SHA1哈希值
static String calculateSHA1(Uint8List data) {
final digest = sha1.convert(data);
return base64Encode(digest.bytes);
}
/// SHA256哈希值
static String calculateSHA256(Uint8List data) {
final digest = sha256.convert(data);
return base64Encode(digest.bytes);
}
///
static bool verifySignature({
required String method,
required String objectKey,
required String bucket,
required String accessKeyId,
required String accessKeySecret,
required Map<String, String> headers,
required String expectedSignature,
String? securityToken,
}) {
final calculatedSignature = generateSignature(
method: method,
objectKey: objectKey,
bucket: bucket,
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
headers: headers,
securityToken: securityToken,
);
return calculatedSignature == expectedSignature;
}
/// OSS头部
static String _buildCanonicalizedOSSHeaders(
Map<String, String> headers, String? securityToken) {
final ossHeaders = <String, String>{};
//
if (securityToken != null) {
ossHeaders['x-oss-security-token'] = securityToken;
}
// x-oss-
for (final entry in headers.entries) {
final lowerKey = entry.key.toLowerCase();
if (lowerKey.startsWith('x-oss-')) {
ossHeaders[lowerKey] = entry.value.trim();
}
}
//
final sortedKeys = ossHeaders.keys.toList()..sort();
return sortedKeys.map((key) => '$key:${ossHeaders[key]}').join('\n');
}
///
static String _buildCanonicalizedResource(String bucket, String objectKey) {
if (bucket.isEmpty && objectKey.isEmpty) {
return '/';
}
// objectKey以/
final normalizedObjectKey =
objectKey.isNotEmpty && objectKey.startsWith('/')
? objectKey
: '/$objectKey';
return '/$bucket$normalizedObjectKey';
}
///
static String _buildCanonicalizedResourceWithParams(
String bucket,
String objectKey,
Map<String, String> params,
) {
final baseResource = _buildCanonicalizedResource(bucket, objectKey);
// OSS签名相关的查询参数
const ossParams = {
'acl',
'cors',
'delete',
'lifecycle',
'location',
'logging',
'notification',
'partNumber',
'policy',
'requestPayment',
'torrent',
'uploadId',
'uploads',
'versionId',
'versioning',
'versions',
'website',
'restore',
'tagging',
'replication',
'encryption',
'inventory',
'select',
'select-type',
};
final filteredParams = <String, String>{};
for (final entry in params.entries) {
if (ossParams.contains(entry.key.toLowerCase())) {
filteredParams[entry.key] = entry.value;
}
}
if (filteredParams.isEmpty) {
return baseResource;
}
final sortedKeys = filteredParams.keys.toList()..sort();
final queryString = sortedKeys.map((key) {
final value = filteredParams[key] ?? '';
return value.isEmpty ? key : '$key=$value';
}).join('&');
return '$baseResource?$queryString';
}
///
static String _buildStringToSign({
required String method,
required String contentMD5,
required String contentType,
required String date,
required String canonicalizedOSSHeaders,
required String canonicalizedResource,
}) {
final components = [
method.toUpperCase(),
contentMD5,
contentType,
date,
canonicalizedOSSHeaders,
canonicalizedResource,
];
return components.join('\n');
}
/// HMAC-SHA1签名
static String _calculateSignature(
String stringToSign, String accessKeySecret) {
final key = utf8.encode(accessKeySecret);
final bytes = utf8.encode(stringToSign);
final hmac = Hmac(sha1, key);
final digest = hmac.convert(bytes);
return base64Encode(digest.bytes);
}
/// HTTP日期格式
static String formatHttpDate(DateTime dateTime) {
const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
final utc = dateTime.toUtc();
final weekday = weekdays[utc.weekday - 1];
final day = utc.day.toString().padLeft(2, '0');
final month = months[utc.month - 1];
final year = utc.year.toString();
final hour = utc.hour.toString().padLeft(2, '0');
final minute = utc.minute.toString().padLeft(2, '0');
final second = utc.second.toString().padLeft(2, '0');
return '$weekday, $day $month $year $hour:$minute:$second GMT';
}
/// HTTP日期格式
static DateTime parseHttpDate(String httpDate) {
// RFC 822Wed, 09 Jun 2021 10:18:14 GMT
final parts = httpDate.split(' ');
if (parts.length < 6) {
throw FormatException('Invalid HTTP date format: $httpDate');
}
const months = {
'Jan': 1,
'Feb': 2,
'Mar': 3,
'Apr': 4,
'May': 5,
'Jun': 6,
'Jul': 7,
'Aug': 8,
'Sep': 9,
'Oct': 10,
'Nov': 11,
'Dec': 12,
};
final day = int.parse(parts[1]);
final month = months[parts[2]] ?? 1;
final year = int.parse(parts[3]);
final timeParts = parts[4].split(':');
final hour = int.parse(timeParts[0]);
final minute = int.parse(timeParts[1]);
final second = int.parse(timeParts[2]);
return DateTime.utc(year, month, day, hour, minute, second);
}
}

View File

@ -0,0 +1,186 @@
///
///
/// OSS认证信息的标准接口
/// - AccessKey/SecretKey
/// - STS临时凭证
/// - URL
/// -
abstract class AuthProvider {
/// AccessKey ID
Future<String> getAccessKeyId();
/// Access Key Secret
Future<String> getAccessKeySecret();
/// Security Token (STS模式)
/// STS模式时返回null
Future<String?> getSecurityToken();
///
/// true表示认证信息有效false表示需要刷新
Future<bool> isValid();
///
///
Future<void> refresh();
///
/// null表示永不过期
Future<DateTime?> getExpiration();
}
///
/// 使AccessKey和SecretKey
class StaticAuthProvider implements AuthProvider {
final String accessKeyId;
final String accessKeySecret;
const StaticAuthProvider({
required this.accessKeyId,
required this.accessKeySecret,
});
@override
Future<String> getAccessKeyId() async => accessKeyId;
@override
Future<String> getAccessKeySecret() async => accessKeySecret;
@override
Future<String?> getSecurityToken() async => null;
@override
Future<bool> isValid() async => true;
@override
Future<void> refresh() async {
//
}
@override
Future<DateTime?> getExpiration() async => null;
}
/// STS认证提供者
/// 使访
class STSAuthProvider implements AuthProvider {
String _accessKeyId;
String _accessKeySecret;
String _securityToken;
DateTime _expiration;
final Future<STSCredentials> Function() _credentialsGetter;
STSAuthProvider({
required String accessKeyId,
required String accessKeySecret,
required String securityToken,
required DateTime expiration,
required Future<STSCredentials> Function() credentialsGetter,
}) : _accessKeyId = accessKeyId,
_accessKeySecret = accessKeySecret,
_securityToken = securityToken,
_expiration = expiration,
_credentialsGetter = credentialsGetter;
@override
Future<String> getAccessKeyId() async {
await _ensureValid();
return _accessKeyId;
}
@override
Future<String> getAccessKeySecret() async {
await _ensureValid();
return _accessKeySecret;
}
@override
Future<String?> getSecurityToken() async {
await _ensureValid();
return _securityToken;
}
@override
Future<bool> isValid() async {
return DateTime.now()
.isBefore(_expiration.subtract(const Duration(minutes: 5)));
}
@override
Future<void> refresh() async {
final credentials = await _credentialsGetter();
_accessKeyId = credentials.accessKeyId;
_accessKeySecret = credentials.accessKeySecret;
_securityToken = credentials.securityToken;
_expiration = credentials.expiration;
}
@override
Future<DateTime?> getExpiration() async => _expiration;
///
Future<void> _ensureValid() async {
if (!await isValid()) {
await refresh();
}
}
}
/// STS凭证模型
class STSCredentials {
final String accessKeyId;
final String accessKeySecret;
final String securityToken;
final DateTime expiration;
const STSCredentials({
required this.accessKeyId,
required this.accessKeySecret,
required this.securityToken,
required this.expiration,
});
factory STSCredentials.fromJson(Map<String, dynamic> json) {
return STSCredentials(
accessKeyId: json['AccessKeyId'] as String,
accessKeySecret: json['AccessKeySecret'] as String,
securityToken: json['SecurityToken'] as String,
expiration: DateTime.parse(json['Expiration'] as String),
);
}
}
/// URL认证提供者
/// 使URL进行上传
class PreSignedAuthProvider implements AuthProvider {
final Future<String> Function(String fileName) _urlGetter;
const PreSignedAuthProvider({
required Future<String> Function(String fileName) urlGetter,
}) : _urlGetter = urlGetter;
/// URL
Future<String> getPreSignedUrl(String fileName) => _urlGetter(fileName);
@override
Future<String> getAccessKeyId() async => throw UnsupportedError(
'PreSignedAuthProvider does not support getAccessKeyId');
@override
Future<String> getAccessKeySecret() async => throw UnsupportedError(
'PreSignedAuthProvider does not support getAccessKeySecret');
@override
Future<String?> getSecurityToken() async => null;
@override
Future<bool> isValid() async => true;
@override
Future<void> refresh() async {
// URL模式不需要刷新认证信息
}
@override
Future<DateTime?> getExpiration() async => null;
}

View File

@ -0,0 +1,150 @@
/// OSS配置提供者接口
///
/// OSS配置信息的标准接口
abstract class ConfigProvider {
/// OSS端点
Future<String> getEndpoint();
///
Future<String> getBucketName();
///
Future<String> getDirectory();
/// 访
/// null使 https://{bucket}.{endpoint}
Future<String?> getDomain();
///
Future<bool> isValid();
///
Future<void> refresh();
}
///
/// 使
class StaticConfigProvider implements ConfigProvider {
final String endpoint;
final String bucketName;
final String directory;
final String? domain;
const StaticConfigProvider({
required this.endpoint,
required this.bucketName,
this.directory = 'uploads',
this.domain,
});
@override
Future<String> getEndpoint() async => endpoint;
@override
Future<String> getBucketName() async => bucketName;
@override
Future<String> getDirectory() async => directory;
@override
Future<String?> getDomain() async => domain;
@override
Future<bool> isValid() async => true;
@override
Future<void> refresh() async {
//
}
}
///
///
class DynamicConfigProvider implements ConfigProvider {
OSSConfig? _config;
DateTime? _lastUpdate;
final Duration _cacheTimeout;
final Future<OSSConfig> Function() _configGetter;
DynamicConfigProvider({
required Future<OSSConfig> Function() configGetter,
Duration cacheTimeout = const Duration(hours: 1),
}) : _configGetter = configGetter,
_cacheTimeout = cacheTimeout;
@override
Future<String> getEndpoint() async {
await _ensureConfig();
return _config!.endpoint;
}
@override
Future<String> getBucketName() async {
await _ensureConfig();
return _config!.bucketName;
}
@override
Future<String> getDirectory() async {
await _ensureConfig();
return _config!.directory;
}
@override
Future<String?> getDomain() async {
await _ensureConfig();
return _config!.domain;
}
@override
Future<bool> isValid() async {
if (_config == null || _lastUpdate == null) return false;
return DateTime.now().difference(_lastUpdate!) < _cacheTimeout;
}
@override
Future<void> refresh() async {
_config = await _configGetter();
_lastUpdate = DateTime.now();
}
///
Future<void> _ensureConfig() async {
if (!await isValid()) {
await refresh();
}
}
}
/// OSS配置模型
class OSSConfig {
final String endpoint;
final String bucketName;
final String directory;
final String? domain;
const OSSConfig({
required this.endpoint,
required this.bucketName,
this.directory = 'uploads',
this.domain,
});
factory OSSConfig.fromJson(Map<String, dynamic> json) {
return OSSConfig(
endpoint: json['endpoint'] as String,
bucketName: json['bucketName'] as String,
directory: json['directory'] as String? ?? 'uploads',
domain: json['domain'] as String?,
);
}
Map<String, dynamic> toJson() {
return {
'endpoint': endpoint,
'bucketName': bucketName,
'directory': directory,
if (domain != null) 'domain': domain,
};
}
}

View File

@ -0,0 +1,66 @@
///
///
///
abstract class ResumeStorage {
///
///
/// [taskId] ID
/// [resumeInfo]
Future<void> saveResumeInfo(String taskId, Map<String, dynamic> resumeInfo);
///
///
/// [taskId] ID
/// null
Future<Map<String, dynamic>?> loadResumeInfo(String taskId);
///
///
/// [taskId] ID
Future<void> deleteResumeInfo(String taskId);
/// ID
Future<List<String>> getAllTaskIds();
///
Future<void> clearAll();
///
Future<int> getStorageSize() async => 0;
}
///
class MemoryResumeStorage implements ResumeStorage {
final Map<String, Map<String, dynamic>> _storage = {};
@override
Future<void> saveResumeInfo(
String taskId, Map<String, dynamic> resumeInfo) async {
_storage[taskId] = Map<String, dynamic>.from(resumeInfo);
}
@override
Future<Map<String, dynamic>?> loadResumeInfo(String taskId) async {
return _storage[taskId];
}
@override
Future<void> deleteResumeInfo(String taskId) async {
_storage.remove(taskId);
}
@override
Future<List<String>> getAllTaskIds() async {
return _storage.keys.toList();
}
@override
Future<void> clearAll() async {
_storage.clear();
}
@override
Future<int> getStorageSize() async {
return _storage.length;
}
}

View File

@ -0,0 +1,67 @@
import '../models/oss_error.dart';
import '../models/upload_result.dart';
///
///
///
class UploadCallbacks {
///
final void Function()? onStart;
///
/// [sent]
/// [total]
final void Function(int sent, int total)? onProgress;
///
/// [result]
final void Function(UploadResult result)? onSuccess;
///
/// [error]
final void Function(OSSError error)? onError;
///
final void Function()? onComplete;
const UploadCallbacks({
this.onStart,
this.onProgress,
this.onSuccess,
this.onError,
this.onComplete,
});
}
///
class DownloadCallbacks {
///
final void Function()? onStart;
///
/// [received]
/// [total]
final void Function(int received, int total)? onProgress;
///
/// [filePath]
final void Function(String filePath)? onSuccess;
///
/// [error]
final void Function(OSSError error)? onError;
///
final void Function()? onComplete;
const DownloadCallbacks({
this.onStart,
this.onProgress,
this.onSuccess,
this.onError,
this.onComplete,
});
}
//
// UploadResult OSSError

View File

@ -0,0 +1,261 @@
/// OSS错误类型枚举
enum OSSErrorType {
///
network,
///
authentication,
///
permission,
///
fileNotFound,
///
configuration,
///
signature,
///
server,
///
client,
///
cancelled,
///
timeout,
///
unknown,
}
/// OSS错误类
class OSSError implements Exception {
///
final OSSErrorType type;
///
final String? code;
///
final String message;
///
final String? details;
/// HTTP状态码
final int? statusCode;
/// ID
final String? requestId;
///
final Object? originalException;
///
final StackTrace? stackTrace;
const OSSError({
required this.type,
required this.message,
this.code,
this.details,
this.statusCode,
this.requestId,
this.originalException,
this.stackTrace,
});
///
factory OSSError.network(String message,
{Object? originalException, StackTrace? stackTrace}) {
return OSSError(
type: OSSErrorType.network,
message: message,
originalException: originalException,
stackTrace: stackTrace,
);
}
///
factory OSSError.authentication(String message, {String? code}) {
return OSSError(
type: OSSErrorType.authentication,
message: message,
code: code,
);
}
///
factory OSSError.permission(String message, {String? code, int? statusCode}) {
return OSSError(
type: OSSErrorType.permission,
message: message,
code: code,
statusCode: statusCode,
);
}
///
factory OSSError.fileNotFound(String message, {String? filePath}) {
return OSSError(
type: OSSErrorType.fileNotFound,
message: message,
details: filePath,
);
}
///
factory OSSError.configuration(String message, {String? details}) {
return OSSError(
type: OSSErrorType.configuration,
message: message,
details: details,
);
}
///
factory OSSError.signature(String message, {String? code}) {
return OSSError(
type: OSSErrorType.signature,
message: message,
code: code,
);
}
///
factory OSSError.server(String message,
{String? code, int? statusCode, String? requestId}) {
return OSSError(
type: OSSErrorType.server,
message: message,
code: code,
statusCode: statusCode,
requestId: requestId,
);
}
///
factory OSSError.client(String message, {String? code, int? statusCode}) {
return OSSError(
type: OSSErrorType.client,
message: message,
code: code,
statusCode: statusCode,
);
}
///
factory OSSError.cancelled(String message) {
return OSSError(
type: OSSErrorType.cancelled,
message: message,
);
}
///
factory OSSError.timeout(String message) {
return OSSError(
type: OSSErrorType.timeout,
message: message,
);
}
///
factory OSSError.unknown(String message,
{Object? originalException, StackTrace? stackTrace}) {
return OSSError(
type: OSSErrorType.unknown,
message: message,
originalException: originalException,
stackTrace: stackTrace,
);
}
/// HTTP响应创建错误
factory OSSError.fromResponse(int statusCode, String? body,
{String? requestId}) {
OSSErrorType type;
String message;
String? code;
if (statusCode >= 400 && statusCode < 500) {
type = OSSErrorType.client;
message = 'Client error: $statusCode';
} else if (statusCode >= 500) {
type = OSSErrorType.server;
message = 'Server error: $statusCode';
} else {
type = OSSErrorType.unknown;
message = 'Unknown error: $statusCode';
}
//
if (body != null && body.isNotEmpty) {
try {
//
// XML或JSON格式的错误信息
message = body;
} catch (e) {
// 使body
message = body;
}
}
return OSSError(
type: type,
message: message,
code: code,
statusCode: statusCode,
requestId: requestId,
);
}
///
bool get isNetworkError => type == OSSErrorType.network;
///
bool get isAuthenticationError => type == OSSErrorType.authentication;
///
bool get isPermissionError => type == OSSErrorType.permission;
///
bool get isCancelledError => type == OSSErrorType.cancelled;
///
bool get isRetryable {
switch (type) {
case OSSErrorType.network:
case OSSErrorType.timeout:
case OSSErrorType.server:
return true;
case OSSErrorType.authentication:
case OSSErrorType.permission:
case OSSErrorType.fileNotFound:
case OSSErrorType.configuration:
case OSSErrorType.signature:
case OSSErrorType.client:
case OSSErrorType.cancelled:
case OSSErrorType.unknown:
return false;
}
}
@override
String toString() {
final buffer = StringBuffer('OSSError(');
buffer.write('type: $type');
buffer.write(', message: $message');
if (code != null) buffer.write(', code: $code');
if (statusCode != null) buffer.write(', statusCode: $statusCode');
if (requestId != null) buffer.write(', requestId: $requestId');
buffer.write(')');
return buffer.toString();
}
}

View File

@ -0,0 +1,423 @@
import 'dart:io';
import 'dart:typed_data';
import '../interfaces/upload_callbacks.dart';
///
class UploadOptions {
///
final UploadCallbacks? callbacks;
///
final bool overwrite;
/// 访
final ACLMode? acl;
///
final StorageClass? storageClass;
/// HTTP头
final Map<String, String>? headers;
///
final Map<String, String>? metadata;
///
final DateTime? expires;
///
final String? cacheControl;
///
final String? contentType;
///
final String? contentEncoding;
///
final String? contentDisposition;
///
final String? contentLanguage;
///
final bool serverSideEncryption;
///
final String? encryptionAlgorithm;
///
final String? encryptionKey;
///
/// 4MB100KB5GB
final int partSize;
/// 线
final int concurrency;
///
final int retryCount;
///
final int retryInterval;
///
final int connectTimeout;
///
final int sendTimeout;
///
final int receiveTimeout;
///
///
final bool enableMultipart;
///
/// 使
final int multipartThreshold;
///
///
final bool enableResume;
///
final void Function(String taskId, int uploadedSize, int totalSize)? onStart;
const UploadOptions({
this.callbacks,
this.overwrite = true,
this.acl,
this.storageClass,
this.headers,
this.metadata,
this.expires,
this.cacheControl,
this.contentType,
this.contentEncoding,
this.contentDisposition,
this.contentLanguage,
this.serverSideEncryption = false,
this.encryptionAlgorithm,
this.encryptionKey,
this.partSize = 4 * 1024 * 1024, // 4MB
this.concurrency = 3,
this.retryCount = 3,
this.retryInterval = 1000,
this.connectTimeout = 60000,
this.sendTimeout = 300000,
this.receiveTimeout = 300000,
this.enableMultipart = true,
this.multipartThreshold = 100 * 1024 * 1024, // 100MB
this.enableResume = false,
this.onStart,
});
///
UploadOptions copyWith({
UploadCallbacks? callbacks,
bool? overwrite,
ACLMode? acl,
StorageClass? storageClass,
Map<String, String>? headers,
Map<String, String>? metadata,
DateTime? expires,
String? cacheControl,
String? contentType,
String? contentEncoding,
String? contentDisposition,
String? contentLanguage,
bool? serverSideEncryption,
String? encryptionAlgorithm,
String? encryptionKey,
int? partSize,
int? concurrency,
int? retryCount,
int? retryInterval,
int? connectTimeout,
int? sendTimeout,
int? receiveTimeout,
bool? enableMultipart,
int? multipartThreshold,
bool? enableResume,
void Function(String taskId, int uploadedSize, int totalSize)? onStart,
}) {
return UploadOptions(
callbacks: callbacks ?? this.callbacks,
overwrite: overwrite ?? this.overwrite,
acl: acl ?? this.acl,
storageClass: storageClass ?? this.storageClass,
headers: headers ?? this.headers,
metadata: metadata ?? this.metadata,
expires: expires ?? this.expires,
cacheControl: cacheControl ?? this.cacheControl,
contentType: contentType ?? this.contentType,
contentEncoding: contentEncoding ?? this.contentEncoding,
contentDisposition: contentDisposition ?? this.contentDisposition,
contentLanguage: contentLanguage ?? this.contentLanguage,
serverSideEncryption: serverSideEncryption ?? this.serverSideEncryption,
encryptionAlgorithm: encryptionAlgorithm ?? this.encryptionAlgorithm,
encryptionKey: encryptionKey ?? this.encryptionKey,
partSize: partSize ?? this.partSize,
concurrency: concurrency ?? this.concurrency,
retryCount: retryCount ?? this.retryCount,
retryInterval: retryInterval ?? this.retryInterval,
connectTimeout: connectTimeout ?? this.connectTimeout,
sendTimeout: sendTimeout ?? this.sendTimeout,
receiveTimeout: receiveTimeout ?? this.receiveTimeout,
enableMultipart: enableMultipart ?? this.enableMultipart,
multipartThreshold: multipartThreshold ?? this.multipartThreshold,
enableResume: enableResume ?? this.enableResume,
onStart: onStart ?? this.onStart,
);
}
///
bool validate() {
if (partSize < 100 * 1024) return false; // 100KB
if (partSize > 5 * 1024 * 1024 * 1024) return false; // 5GB
if (concurrency < 1 || concurrency > 10) return false;
if (retryCount < 0 || retryCount > 10) return false;
if (retryInterval < 0) return false;
if (connectTimeout < 0) return false;
if (sendTimeout < 0) return false;
if (receiveTimeout < 0) return false;
if (multipartThreshold < partSize) return false;
return true;
}
/// JSON
Map<String, dynamic> toJson() {
return {
'overwrite': overwrite,
'acl': acl?.value,
'storageClass': storageClass?.value,
'headers': headers,
'metadata': metadata,
'expires': expires?.toIso8601String(),
'cacheControl': cacheControl,
'contentType': contentType,
'contentEncoding': contentEncoding,
'contentDisposition': contentDisposition,
'contentLanguage': contentLanguage,
'serverSideEncryption': serverSideEncryption,
'encryptionAlgorithm': encryptionAlgorithm,
'encryptionKey': encryptionKey,
'partSize': partSize,
'concurrency': concurrency,
'retryCount': retryCount,
'retryInterval': retryInterval,
'connectTimeout': connectTimeout,
'sendTimeout': sendTimeout,
'receiveTimeout': receiveTimeout,
'enableMultipart': enableMultipart,
'multipartThreshold': multipartThreshold,
'enableResume': enableResume,
};
}
/// JSON创建实例
factory UploadOptions.fromJson(Map<String, dynamic> json) {
return UploadOptions(
overwrite: json['overwrite'] as bool? ?? true,
acl: json['acl'] != null
? ACLMode.values.firstWhere((e) => e.value == json['acl'])
: null,
storageClass: json['storageClass'] != null
? StorageClass.values
.firstWhere((e) => e.value == json['storageClass'])
: null,
headers: json['headers'] != null
? Map<String, String>.from(json['headers'] as Map)
: null,
metadata: json['metadata'] != null
? Map<String, String>.from(json['metadata'] as Map)
: null,
expires: json['expires'] != null
? DateTime.parse(json['expires'] as String)
: null,
cacheControl: json['cacheControl'] as String?,
contentType: json['contentType'] as String?,
contentEncoding: json['contentEncoding'] as String?,
contentDisposition: json['contentDisposition'] as String?,
contentLanguage: json['contentLanguage'] as String?,
serverSideEncryption: json['serverSideEncryption'] as bool? ?? false,
encryptionAlgorithm: json['encryptionAlgorithm'] as String?,
encryptionKey: json['encryptionKey'] as String?,
partSize: json['partSize'] as int? ?? 4 * 1024 * 1024,
concurrency: json['concurrency'] as int? ?? 3,
retryCount: json['retryCount'] as int? ?? 3,
retryInterval: json['retryInterval'] as int? ?? 1000,
connectTimeout: json['connectTimeout'] as int? ?? 60000,
sendTimeout: json['sendTimeout'] as int? ?? 300000,
receiveTimeout: json['receiveTimeout'] as int? ?? 300000,
enableMultipart: json['enableMultipart'] as bool? ?? true,
multipartThreshold:
json['multipartThreshold'] as int? ?? 100 * 1024 * 1024,
enableResume: json['enableResume'] as bool? ?? false,
);
}
}
/// 访
enum ACLMode {
///
private('private'),
///
publicRead('public-read'),
///
publicReadWrite('public-read-write'),
///
bucketOwnerRead('bucket-owner-read'),
///
bucketOwnerFullControl('bucket-owner-full-control'),
///
inherited('default');
const ACLMode(this.value);
final String value;
}
///
enum StorageClass {
///
standard('Standard'),
/// 访
infrequentAccess('IA'),
///
archive('Archive'),
///
coldArchive('ColdArchive'),
///
deepColdArchive('DeepColdArchive');
const StorageClass(this.value);
final String value;
}
///
abstract class UploadDataSource {
///
int get size;
///
String get fileName;
/// MIME类型
String? get mimeType;
///
String? get filePath;
///
Stream<Uint8List> openRead([int? start, int? end]);
///
Future<Uint8List> readAsBytes();
///
static FilePathDataSource fromFilePath(String filePath,
{String? fileName, String? mimeType}) {
return FilePathDataSource(filePath, fileName: fileName, mimeType: mimeType);
}
///
static BytesDataSource fromBytes(Uint8List bytes, String fileName,
{String? mimeType}) {
return BytesDataSource(bytes, fileName, mimeType: mimeType);
}
}
///
class BytesDataSource implements UploadDataSource {
final Uint8List _bytes;
final String _fileName;
final String? _mimeType;
BytesDataSource(this._bytes, this._fileName, {String? mimeType})
: _mimeType = mimeType;
@override
int get size => _bytes.length;
@override
String get fileName => _fileName;
@override
String? get mimeType => _mimeType;
@override
String? get filePath => null; //
@override
Stream<Uint8List> openRead([int? start, int? end]) async* {
final startIndex = start ?? 0;
final endIndex = end ?? _bytes.length;
yield _bytes.sublist(startIndex, endIndex);
}
@override
Future<Uint8List> readAsBytes() async => _bytes;
}
///
class FilePathDataSource implements UploadDataSource {
final String _filePath;
final String? _fileName;
final String? _mimeType;
int? _cachedSize;
FilePathDataSource(this._filePath, {String? fileName, String? mimeType})
: _fileName = fileName,
_mimeType = mimeType;
@override
int get size {
if (_cachedSize == null) {
try {
final file = File(_filePath);
_cachedSize = file.lengthSync();
} catch (e) {
_cachedSize = 0;
}
}
return _cachedSize!;
}
@override
String get fileName => _fileName ?? _filePath.split('/').last;
@override
String? get mimeType => _mimeType;
@override
String? get filePath => _filePath;
@override
Stream<Uint8List> openRead([int? start, int? end]) async* {
final file = File(_filePath);
final stream = file.openRead(start, end);
await for (final chunk in stream) {
yield Uint8List.fromList(chunk);
}
}
@override
Future<Uint8List> readAsBytes() async {
final file = File(_filePath);
return await file.readAsBytes();
}
}

View File

@ -0,0 +1,322 @@
///
class UploadResult {
/// OSS URL
final String url;
/// bucket和目录
final String objectKey;
///
final int fileSize;
/// ETag
final String? etag;
///
final int uploadDuration;
/// MIME类型
final String? mimeType;
/// MD5哈希值
final String? md5Hash;
///
final DateTime uploadTime;
///
final Map<String, String>? metadata;
const UploadResult({
required this.url,
required this.objectKey,
required this.fileSize,
required this.uploadDuration,
required this.uploadTime,
this.etag,
this.mimeType,
this.md5Hash,
this.metadata,
});
///
factory UploadResult.fromResponse({
required String url,
required String objectKey,
required int fileSize,
required int uploadDuration,
Map<String, dynamic>? headers,
String? mimeType,
String? md5Hash,
Map<String, String>? metadata,
}) {
String? etag;
if (headers != null) {
// ETag
final etagHeader = headers['etag'] ?? headers['ETag'];
if (etagHeader is List && etagHeader.isNotEmpty) {
etag = etagHeader.first.toString().replaceAll('"', '');
} else if (etagHeader is String) {
etag = etagHeader.replaceAll('"', '');
}
}
return UploadResult(
url: url,
objectKey: objectKey,
fileSize: fileSize,
uploadDuration: uploadDuration,
uploadTime: DateTime.now(),
etag: etag,
mimeType: mimeType,
md5Hash: md5Hash,
metadata: metadata,
);
}
/// JSON
Map<String, dynamic> toJson() {
return {
'url': url,
'objectKey': objectKey,
'fileSize': fileSize,
'uploadDuration': uploadDuration,
'uploadTime': uploadTime.toIso8601String(),
if (etag != null) 'etag': etag,
if (mimeType != null) 'mimeType': mimeType,
if (md5Hash != null) 'md5Hash': md5Hash,
if (metadata != null) 'metadata': metadata,
};
}
/// JSON创建上传结果
factory UploadResult.fromJson(Map<String, dynamic> json) {
return UploadResult(
url: json['url'] as String,
objectKey: json['objectKey'] as String,
fileSize: json['fileSize'] as int,
uploadDuration: json['uploadDuration'] as int,
uploadTime: DateTime.parse(json['uploadTime'] as String),
etag: json['etag'] as String?,
mimeType: json['mimeType'] as String?,
md5Hash: json['md5Hash'] as String?,
metadata:
(json['metadata'] as Map<String, dynamic>?)?.cast<String, String>(),
);
}
@override
String toString() {
return 'UploadResult(url: $url, objectKey: $objectKey, fileSize: $fileSize, uploadDuration: ${uploadDuration}ms)';
}
}
///
class MultipartUploadResult extends UploadResult {
///
final int partCount;
/// ID
final String uploadId;
///
final List<PartInfo> parts;
const MultipartUploadResult({
required super.url,
required super.objectKey,
required super.fileSize,
required super.uploadDuration,
required super.uploadTime,
required this.partCount,
required this.uploadId,
required this.parts,
super.etag,
super.mimeType,
super.md5Hash,
super.metadata,
});
@override
Map<String, dynamic> toJson() {
final json = super.toJson();
json.addAll({
'partCount': partCount,
'uploadId': uploadId,
'parts': parts.map((part) => part.toJson()).toList(),
});
return json;
}
}
///
class PartInfo {
///
final int partNumber;
/// ETag
final String etag;
///
final int size;
///
final int uploadDuration;
const PartInfo({
required this.partNumber,
required this.etag,
required this.size,
required this.uploadDuration,
});
Map<String, dynamic> toJson() {
return {
'partNumber': partNumber,
'etag': etag,
'size': size,
'uploadDuration': uploadDuration,
};
}
factory PartInfo.fromJson(Map<String, dynamic> json) {
return PartInfo(
partNumber: json['partNumber'] as int,
etag: json['etag'] as String,
size: json['size'] as int,
uploadDuration: json['uploadDuration'] as int,
);
}
}
///
class ResumeUploadInfo {
/// ID
final String taskId;
///
final String filePath;
/// OSS中的文件路径
final String objectKey;
/// ID
final String uploadId;
///
final int totalSize;
///
final int partSize;
///
final List<PartInfo> completedParts;
///
final DateTime createdAt;
///
final DateTime updatedAt;
///
final Map<String, dynamic> uploadOptions;
const ResumeUploadInfo({
required this.taskId,
required this.filePath,
required this.objectKey,
required this.uploadId,
required this.totalSize,
required this.partSize,
required this.completedParts,
required this.createdAt,
required this.updatedAt,
required this.uploadOptions,
});
///
int get uploadedSize {
return completedParts.fold<int>(0, (sum, part) => sum + part.size);
}
/// 0.0 - 1.0
double get progress {
if (totalSize == 0) return 0.0;
return uploadedSize / totalSize;
}
///
int get nextPartNumber {
if (completedParts.isEmpty) return 1;
final maxPartNumber =
completedParts.map((p) => p.partNumber).reduce((a, b) => a > b ? a : b);
return maxPartNumber + 1;
}
///
bool isPartCompleted(int partNumber) {
return completedParts.any((part) => part.partNumber == partNumber);
}
///
ResumeUploadInfo addCompletedPart(PartInfo partInfo) {
final updatedParts = List<PartInfo>.from(completedParts);
//
updatedParts.removeWhere((part) => part.partNumber == partInfo.partNumber);
updatedParts.add(partInfo);
updatedParts.sort((a, b) => a.partNumber.compareTo(b.partNumber));
return ResumeUploadInfo(
taskId: taskId,
filePath: filePath,
objectKey: objectKey,
uploadId: uploadId,
totalSize: totalSize,
partSize: partSize,
completedParts: updatedParts,
createdAt: createdAt,
updatedAt: DateTime.now(),
uploadOptions: uploadOptions,
);
}
/// JSON
Map<String, dynamic> toJson() {
return {
'taskId': taskId,
'filePath': filePath,
'objectKey': objectKey,
'uploadId': uploadId,
'totalSize': totalSize,
'partSize': partSize,
'completedParts': completedParts.map((part) => part.toJson()).toList(),
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'uploadOptions': uploadOptions,
};
}
/// JSON创建实例
factory ResumeUploadInfo.fromJson(Map<String, dynamic> json) {
return ResumeUploadInfo(
taskId: json['taskId'] as String,
filePath: json['filePath'] as String,
objectKey: json['objectKey'] as String,
uploadId: json['uploadId'] as String,
totalSize: json['totalSize'] as int,
partSize: json['partSize'] as int,
completedParts: (json['completedParts'] as List)
.map((part) => PartInfo.fromJson(part as Map<String, dynamic>))
.toList(),
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
uploadOptions: json['uploadOptions'] as Map<String, dynamic>,
);
}
@override
String toString() {
return 'ResumeUploadInfo(taskId: $taskId, objectKey: $objectKey, progress: ${(progress * 100).toStringAsFixed(1)}%)';
}
}

View File

@ -0,0 +1,433 @@
import 'package:mime/mime.dart';
import 'package:uuid/uuid.dart';
///
class YxOSSFileUtils {
static const _uuid = Uuid();
///
///
/// [originalFileName]
/// [preserveExtension]
/// [prefix]
/// [suffix]
static String generateUniqueFileName({
String? originalFileName,
bool preserveExtension = true,
String? prefix,
String? suffix,
}) {
final uuid = _uuid.v4().replaceAll('-', '');
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
String fileName = '${prefix ?? ''}${timestamp}_$uuid${suffix ?? ''}';
if (preserveExtension && originalFileName != null) {
final extension = getFileExtension(originalFileName);
if (extension.isNotEmpty) {
fileName += '.$extension';
}
}
return fileName;
}
///
///
/// [fileName]
static String getFileExtension(String fileName) {
final lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == fileName.length - 1) {
return '';
}
return fileName.substring(lastDotIndex + 1).toLowerCase();
}
///
///
/// [fileName]
static String getFileNameWithoutExtension(String fileName) {
final lastDotIndex = fileName.lastIndexOf('.');
final lastSlashIndex = fileName.lastIndexOf('/');
final lastBackslashIndex = fileName.lastIndexOf('\\');
final nameStartIndex = [lastSlashIndex, lastBackslashIndex]
.where((index) => index != -1)
.fold(-1, (prev, current) => prev > current ? prev : current) +
1;
final nameEndIndex = lastDotIndex == -1 ? fileName.length : lastDotIndex;
return fileName.substring(nameStartIndex, nameEndIndex);
}
/// MIME类型
///
/// [fileName]
static String? guessMimeType(String fileName) {
return lookupMimeType(fileName);
}
/// MIME类型判断是否为图片文件
///
/// [mimeType] MIME类型
static bool isImageMimeType(String? mimeType) {
if (mimeType == null) return false;
return mimeType.startsWith('image/');
}
///
///
/// [fileName]
static bool isImageFile(String fileName) {
final extension = getFileExtension(fileName);
const imageExtensions = {
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
'svg',
'ico',
'tiff',
'tif'
};
return imageExtensions.contains(extension);
}
/// MIME类型判断是否为视频文件
///
/// [mimeType] MIME类型
static bool isVideoMimeType(String? mimeType) {
if (mimeType == null) return false;
return mimeType.startsWith('video/');
}
///
///
/// [fileName]
static bool isVideoFile(String fileName) {
final extension = getFileExtension(fileName);
const videoExtensions = {
'mp4',
'avi',
'mov',
'wmv',
'flv',
'webm',
'mkv',
'm4v',
'3gp',
'rmvb'
};
return videoExtensions.contains(extension);
}
/// MIME类型判断是否为音频文件
///
/// [mimeType] MIME类型
static bool isAudioMimeType(String? mimeType) {
if (mimeType == null) return false;
return mimeType.startsWith('audio/');
}
///
///
/// [fileName]
static bool isAudioFile(String fileName) {
final extension = getFileExtension(fileName);
const audioExtensions = {
'mp3',
'wav',
'flac',
'aac',
'ogg',
'wma',
'm4a',
'amr',
'ape'
};
return audioExtensions.contains(extension);
}
///
///
/// [fileName]
static bool isDocumentFile(String fileName) {
final extension = getFileExtension(fileName);
const documentExtensions = {
'pdf',
'doc',
'docx',
'xls',
'xlsx',
'ppt',
'pptx',
'txt',
'rtf',
'odt'
};
return documentExtensions.contains(extension);
}
///
///
/// [fileName]
static bool isArchiveFile(String fileName) {
final extension = getFileExtension(fileName);
const archiveExtensions = {
'zip',
'rar',
'7z',
'tar',
'gz',
'bz2',
'xz',
'tar.gz',
'tar.bz2'
};
return archiveExtensions.contains(extension);
}
///
///
/// [bytes]
/// [decimals]
static String formatFileSize(int bytes, {int decimals = 1}) {
if (bytes <= 0) return '0 B';
const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
final i = (bytes.bitLength - 1) ~/ 10;
final size = bytes / (1 << (i * 10));
return '${size.toStringAsFixed(decimals)} ${suffixes[i]}';
}
///
///
/// [sizeString] "1.5 MB"
static int? parseFileSize(String sizeString) {
final regex =
RegExp(r'^(\d+(?:\.\d+)?)\s*([KMGTPE]?B?)$', caseSensitive: false);
final match = regex.firstMatch(sizeString.trim());
if (match == null) return null;
final value = double.tryParse(match.group(1)!);
if (value == null) return null;
final unit = match.group(2)?.toUpperCase() ?? 'B';
const multipliers = {
'B': 1,
'KB': 1024,
'MB': 1024 * 1024,
'GB': 1024 * 1024 * 1024,
'TB': 1024 * 1024 * 1024 * 1024,
'PB': 1024 * 1024 * 1024 * 1024 * 1024,
};
final multiplier = multipliers[unit] ?? 1;
return (value * multiplier).round();
}
///
///
/// [fileSize]
/// [partSize]
static List<PartRange> calculatePartRanges(int fileSize, int partSize) {
if (fileSize <= 0 || partSize <= 0) {
return [];
}
final parts = <PartRange>[];
final totalParts = (fileSize / partSize).ceil();
for (int i = 0; i < totalParts; i++) {
final start = i * partSize;
final end = (i == totalParts - 1) ? fileSize - 1 : start + partSize - 1;
parts.add(PartRange(
partNumber: i + 1,
start: start,
end: end,
size: end - start + 1,
));
}
return parts;
}
///
///
/// [fileName]
static bool isValidFileName(String fileName) {
if (fileName.isEmpty || fileName.length > 255) {
return false;
}
//
const invalidChars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
for (final char in invalidChars) {
if (fileName.contains(char)) {
return false;
}
}
// Windows
const reservedNames = [
'CON',
'PRN',
'AUX',
'NUL',
'COM1',
'COM2',
'COM3',
'COM4',
'COM5',
'COM6',
'COM7',
'COM8',
'COM9',
'LPT1',
'LPT2',
'LPT3',
'LPT4',
'LPT5',
'LPT6',
'LPT7',
'LPT8',
'LPT9'
];
final nameWithoutExt = getFileNameWithoutExtension(fileName).toUpperCase();
if (reservedNames.contains(nameWithoutExt)) {
return false;
}
//
if (fileName.endsWith('.') || fileName.endsWith(' ')) {
return false;
}
return true;
}
/// OSS对象键是否合法
///
/// [objectKey] OSS对象键
static bool isValidObjectKey(String objectKey) {
if (objectKey.isEmpty || objectKey.length > 1024) {
return false;
}
// OSS对象键的非法字符
const invalidChars = ['\\', ':', '*', '?', '"', '<', '>', '|'];
for (final char in invalidChars) {
if (objectKey.contains(char)) {
return false;
}
}
// OSS不允许
if (objectKey.startsWith('/')) {
return false;
}
//
if (objectKey.contains('//')) {
return false;
}
//
final segments = objectKey.split('/');
for (final segment in segments) {
if (segment.isEmpty) continue; //
// /
if (segment.startsWith('.') ||
segment.endsWith('.') ||
segment.startsWith(' ') ||
segment.endsWith(' ')) {
return false;
}
}
return true;
}
///
///
/// [fileName]
/// [replacement]
static String sanitizeFileName(String fileName, {String replacement = '_'}) {
if (fileName.isEmpty) return fileName;
//
const invalidChars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
String sanitized = fileName;
for (final char in invalidChars) {
sanitized = sanitized.replaceAll(char, replacement);
}
//
sanitized = sanitized.trim().replaceAll(RegExp(r'^\.+|\.+$'), '');
//
if (sanitized.length > 255) {
final extension = getFileExtension(sanitized);
final maxNameLength =
255 - (extension.isEmpty ? 0 : extension.length + 1);
final nameWithoutExt = getFileNameWithoutExtension(sanitized);
sanitized = nameWithoutExt.substring(0, maxNameLength);
if (extension.isNotEmpty) {
sanitized += '.$extension';
}
}
return sanitized.isEmpty ? 'file' : sanitized;
}
}
///
class PartRange {
/// 1
final int partNumber;
///
final int start;
///
final int end;
///
final int size;
const PartRange({
required this.partNumber,
required this.start,
required this.end,
required this.size,
});
@override
String toString() {
return 'PartRange(partNumber: $partNumber, start: $start, end: $end, size: $size)';
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is PartRange &&
other.partNumber == partNumber &&
other.start == start &&
other.end == end &&
other.size == size;
}
@override
int get hashCode {
return partNumber.hashCode ^ start.hashCode ^ end.hashCode ^ size.hashCode;
}
}

View File

@ -0,0 +1,353 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import '../core/yx_oss_config.dart';
import '../models/oss_error.dart';
/// HTTP工具类
class HttpUtils {
/// Dio实例
static Dio createDio({
TimeoutConfig? timeoutConfig,
LogConfig? logConfig,
Map<String, String>? defaultHeaders,
}) {
final dio = Dio(BaseOptions(
connectTimeout:
Duration(milliseconds: timeoutConfig?.connectTimeout ?? 60000),
sendTimeout: Duration(milliseconds: timeoutConfig?.sendTimeout ?? 300000),
receiveTimeout:
Duration(milliseconds: timeoutConfig?.receiveTimeout ?? 300000),
headers: defaultHeaders,
responseType: ResponseType.bytes,
));
//
if (logConfig?.enabled == true) {
dio.interceptors.add(_createLogInterceptor(logConfig!));
}
//
dio.interceptors.add(_RetryInterceptor());
//
dio.interceptors.add(_ErrorInterceptor());
return dio;
}
///
static Interceptor _createLogInterceptor(LogConfig config) {
return InterceptorsWrapper(
onRequest: (options, handler) {
if (config.logRequest && config.level <= LogLevel.debug) {
_log('HTTP Request: ${options.method} ${options.uri}', LogLevel.debug,
config);
if (options.headers.isNotEmpty) {
_log('Request Headers: ${options.headers}', LogLevel.debug, config);
}
}
handler.next(options);
},
onResponse: (response, handler) {
if (config.logResponse && config.level <= LogLevel.debug) {
_log(
'HTTP Response: ${response.statusCode} ${response.statusMessage}',
LogLevel.debug,
config);
if (response.headers.map.isNotEmpty) {
_log('Response Headers: ${response.headers.map}', LogLevel.debug,
config);
}
}
handler.next(response);
},
onError: (error, handler) {
if (config.logError && config.level <= LogLevel.error) {
_log('HTTP Error: ${error.message}', LogLevel.error, config);
if (error.response != null) {
_log(
'Error Response: ${error.response?.statusCode} ${error.response?.data}',
LogLevel.error,
config);
}
}
handler.next(error);
},
);
}
///
static void _log(String message, LogLevel level, LogConfig config) {
if (config.customHandler != null) {
config.customHandler!(message, level);
} else {
//
print('[YX_OSS] ${level.name.toUpperCase()}: $message');
}
}
/// Range头部
static String buildRangeHeader(int start, int end) {
return 'bytes=$start-$end';
}
/// Content-Range头部
static ContentRange? parseContentRange(String? contentRangeHeader) {
if (contentRangeHeader == null) return null;
final regex = RegExp(r'bytes (\d+)-(\d+)/(\d+|\*)');
final match = regex.firstMatch(contentRangeHeader);
if (match == null) return null;
final start = int.parse(match.group(1)!);
final end = int.parse(match.group(2)!);
final totalString = match.group(3)!;
final total = totalString == '*' ? null : int.parse(totalString);
return ContentRange(start: start, end: end, total: total);
}
/// URL
static String buildMultipartUploadUrl({
required String endpoint,
required String bucket,
required String objectKey,
String? uploadId,
int? partNumber,
}) {
final uri = Uri.https('$bucket.$endpoint', '/$objectKey');
final queryParams = <String, String>{};
if (uploadId != null) {
queryParams['uploadId'] = uploadId;
}
if (partNumber != null) {
queryParams['partNumber'] = partNumber.toString();
}
if (uploadId == null && partNumber == null) {
queryParams['uploads'] = '';
}
if (queryParams.isNotEmpty) {
final queryString = queryParams.entries
.map((e) => e.value.isEmpty ? e.key : '${e.key}=${e.value}')
.join('&');
return '${uri.toString()}?$queryString';
}
return uri.toString();
}
/// ETag
static String? extractETag(Response response) {
final etag =
response.headers.value('etag') ?? response.headers.value('ETag');
return etag?.replaceAll('"', '');
}
/// ID
static String? extractRequestId(Response response) {
return response.headers.value('x-oss-request-id') ??
response.headers.value('X-OSS-Request-ID');
}
/// HTTP状态码是否表示成功
static bool isSuccessStatusCode(int? statusCode) {
return statusCode != null && statusCode >= 200 && statusCode < 300;
}
///
static bool isRetryableStatusCode(int? statusCode) {
if (statusCode == null) return false;
// 5xx
if (statusCode >= 500 && statusCode < 600) return true;
// 4xx错误也可以重试
const retryable4xxCodes = {408, 409, 429}; //
return retryable4xxCodes.contains(statusCode);
}
///
static Stream<Uint8List> createUploadStream(
Uint8List data, {
int? start,
int? end,
int chunkSize = 8192,
}) async* {
final startIndex = start ?? 0;
final endIndex = end ?? data.length;
final length = endIndex - startIndex;
for (int i = 0; i < length; i += chunkSize) {
final chunkEnd = (i + chunkSize > length) ? length : i + chunkSize;
yield data.sublist(startIndex + i, startIndex + chunkEnd);
}
}
///
static double calculateProgress(int sent, int total) {
if (total <= 0) return 0.0;
return (sent / total).clamp(0.0, 1.0);
}
///
static String formatTransferSpeed(int bytesPerSecond) {
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
double speed = bytesPerSecond.toDouble();
int unitIndex = 0;
while (speed >= 1024 && unitIndex < units.length - 1) {
speed /= 1024;
unitIndex++;
}
return '${speed.toStringAsFixed(1)} ${units[unitIndex]}';
}
///
static Duration? estimateRemainingTime(
int sent, int total, int bytesPerSecond) {
if (total <= sent || bytesPerSecond <= 0) return null;
final remaining = total - sent;
final secondsRemaining = remaining / bytesPerSecond;
return Duration(seconds: secondsRemaining.round());
}
}
/// Content-Range解析结果
class ContentRange {
final int start;
final int end;
final int? total;
const ContentRange({
required this.start,
required this.end,
this.total,
});
int get size => end - start + 1;
@override
String toString() {
return 'ContentRange(start: $start, end: $end, total: $total)';
}
}
///
class _RetryInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
final retryConfig = err.requestOptions.extra['retryConfig'] as RetryConfig?;
if (retryConfig == null || !_shouldRetry(err, retryConfig)) {
handler.next(err);
return;
}
final retryAttempt =
(err.requestOptions.extra['retryAttempt'] as int?) ?? 0;
if (retryAttempt >= retryConfig.maxRetries) {
handler.next(err);
return;
}
//
final delay = retryConfig.calculateInterval(retryAttempt);
await Future.delayed(Duration(milliseconds: delay));
//
err.requestOptions.extra['retryAttempt'] = retryAttempt + 1;
try {
final response = await Dio().fetch(err.requestOptions);
handler.resolve(response);
} catch (e) {
if (e is DioException) {
handler.next(e);
} else {
handler.next(DioException(
requestOptions: err.requestOptions,
error: e,
));
}
}
}
bool _shouldRetry(DioException error, RetryConfig config) {
//
if (error.type == DioExceptionType.cancel) return false;
//
final statusCode = error.response?.statusCode;
if (statusCode != null && !HttpUtils.isRetryableStatusCode(statusCode)) {
return false;
}
//
if (config.retryableErrors.isNotEmpty) {
return config.retryableErrors.contains(error.type.runtimeType);
}
//
return error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.sendTimeout ||
error.type == DioExceptionType.receiveTimeout ||
error.type == DioExceptionType.connectionError;
}
}
///
class _ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final ossError = _convertToOSSError(err);
// OSS错误附加到原始错误中
err.requestOptions.extra['ossError'] = ossError;
handler.next(err);
}
OSSError _convertToOSSError(DioException error) {
final statusCode = error.response?.statusCode;
final requestId = HttpUtils.extractRequestId(error.response!);
switch (error.type) {
case DioExceptionType.connectionTimeout:
return OSSError.timeout('Connection timeout');
case DioExceptionType.sendTimeout:
return OSSError.timeout('Send timeout');
case DioExceptionType.receiveTimeout:
return OSSError.timeout('Receive timeout');
case DioExceptionType.connectionError:
return OSSError.network('Connection error: ${error.message}',
originalException: error.error);
case DioExceptionType.cancel:
return OSSError.cancelled('Request cancelled');
case DioExceptionType.badResponse:
if (statusCode != null) {
return OSSError.fromResponse(
statusCode, error.response?.data?.toString(),
requestId: requestId);
}
return OSSError.server('Bad response: ${error.message}');
default:
return OSSError.unknown('Unknown error: ${error.message}',
originalException: error.error);
}
}
}

View File

@ -0,0 +1,196 @@
import 'dart:convert';
import 'dart:io';
import '../interfaces/resume_storage.dart';
// path_provider
// 使 pubspec.yaml path_provider: ^2.1.0
///
class LocalResumeStorage implements ResumeStorage {
static const String _storageDir = 'yx_oss_resume';
static const String _fileExtension = '.json';
Directory? _cacheDirectory;
///
Future<Directory> get cacheDirectory async {
if (_cacheDirectory != null) {
return _cacheDirectory!;
}
// 使 path_provider
// getApplicationDocumentsDirectory()
final tempDir = Directory.systemTemp;
_cacheDirectory = Directory('${tempDir.path}/$_storageDir');
//
if (!await _cacheDirectory!.exists()) {
await _cacheDirectory!.create(recursive: true);
}
return _cacheDirectory!;
}
///
Future<File> _getTaskFile(String taskId) async {
final dir = await cacheDirectory;
return File('${dir.path}/$taskId$_fileExtension');
}
@override
Future<void> saveResumeInfo(
String taskId, Map<String, dynamic> resumeInfo) async {
try {
final file = await _getTaskFile(taskId);
final jsonString = jsonEncode(resumeInfo);
await file.writeAsString(jsonString);
} catch (e) {
throw Exception('Failed to save resume info for task $taskId: $e');
}
}
@override
Future<Map<String, dynamic>?> loadResumeInfo(String taskId) async {
try {
final file = await _getTaskFile(taskId);
if (!await file.exists()) {
return null;
}
final jsonString = await file.readAsString();
return jsonDecode(jsonString) as Map<String, dynamic>;
} catch (e) {
//
try {
final file = await _getTaskFile(taskId);
if (await file.exists()) {
await file.delete();
}
} catch (_) {
//
}
return null;
}
}
@override
Future<void> deleteResumeInfo(String taskId) async {
try {
final file = await _getTaskFile(taskId);
if (await file.exists()) {
await file.delete();
}
} catch (e) {
//
}
}
@override
Future<List<String>> getAllTaskIds() async {
try {
final dir = await cacheDirectory;
if (!await dir.exists()) {
return [];
}
final files = await dir.list().toList();
final taskIds = <String>[];
for (final file in files) {
if (file is File && file.path.endsWith(_fileExtension)) {
final fileName = file.path.split('/').last;
final taskId =
fileName.substring(0, fileName.length - _fileExtension.length);
taskIds.add(taskId);
}
}
return taskIds;
} catch (e) {
return [];
}
}
@override
Future<void> clearAll() async {
try {
final dir = await cacheDirectory;
if (await dir.exists()) {
await dir.delete(recursive: true);
}
_cacheDirectory = null; //
} catch (e) {
//
}
}
@override
Future<int> getStorageSize() async {
try {
final dir = await cacheDirectory;
if (!await dir.exists()) {
return 0;
}
int totalSize = 0;
final files = await dir.list().toList();
for (final file in files) {
if (file is File) {
final stat = await file.stat();
totalSize += stat.size;
}
}
return totalSize;
} catch (e) {
return 0;
}
}
///
///
/// [maxAge]
Future<void> cleanupExpiredTasks(int maxAge) async {
try {
final dir = await cacheDirectory;
if (!await dir.exists()) {
return;
}
final cutoffTime = DateTime.now().subtract(Duration(days: maxAge));
final files = await dir.list().toList();
for (final file in files) {
if (file is File && file.path.endsWith(_fileExtension)) {
final stat = await file.stat();
if (stat.modified.isBefore(cutoffTime)) {
try {
await file.delete();
} catch (_) {
//
}
}
}
}
} catch (e) {
//
}
}
///
Future<int> getTaskFileSize(String taskId) async {
try {
final file = await _getTaskFile(taskId);
if (await file.exists()) {
final stat = await file.stat();
return stat.size;
}
return 0;
} catch (e) {
return 0;
}
}
}

View File

@ -0,0 +1,29 @@
import '../core/yx_oss_config.dart';
/// Lightweight logger that respects LogConfig and LogLevel
class OssLogger {
final LogConfig config;
const OssLogger(this.config);
void debug(String message) => _log(message, LogLevel.debug);
void info(String message) => _log(message, LogLevel.info);
void warn(String message) => _log(message, LogLevel.warning);
void error(String message) => _log(message, LogLevel.error);
void _log(String message, LogLevel level) {
if (!config.enabled) return;
if (level < config.level) return;
if (config.customHandler != null) {
config.customHandler!(message, level);
return;
}
// Default console output
// Keep a consistent tag for easy grep in app logs
// Note: keep print for default fallback only
// ignore: avoid_print
print('[YX_OSS] ${level.name.toUpperCase()}: $message');
}
}

31
lib/yx_oss.dart Normal file
View File

@ -0,0 +1,31 @@
/// YX OSS Library
///
/// A pure OSS (Object Storage Service) client library for Flutter applications.
/// Focused on core upload/download functionality with minimal dependencies.
///
/// Core Features:
/// - Multiple authentication providers (Static, STS, PreSigned)
/// - Resume upload support with local storage
/// - Progress monitoring and cancellation
/// - Retry mechanism with exponential backoff
/// - Type-safe error handling
/// - Clean architecture with dependency injection
library;
// Adapters
export 'src/adapters/project_integration_adapter.dart';
// Core classes
export 'src/core/yx_oss_client.dart';
export 'src/core/yx_oss_config.dart';
// Interfaces
export 'src/interfaces/auth_provider.dart';
export 'src/interfaces/config_provider.dart';
export 'src/interfaces/resume_storage.dart';
export 'src/interfaces/upload_callbacks.dart';
export 'src/models/oss_error.dart';
// Models
export 'src/models/upload_options.dart';
export 'src/models/upload_result.dart';
// Utils
export 'src/utils/file_utils.dart';
export 'src/utils/local_resume_storage.dart';

349
pubspec.lock Normal file
View File

@ -0,0 +1,349 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.1"
crypto:
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.6"
dio:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.9"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.9"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
dependency: "direct main"
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.16.0"
mime:
dependency: "direct main"
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.6"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "993381400e94d18469750e5b9dcb8206f15bc09f9da86b9e44a9b0092a0066db"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.18"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
url: "https://pub.flutter-io.cn"
source: hosted
version: "15.0.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.29.0"

26
pubspec.yaml Normal file
View File

@ -0,0 +1,26 @@
name: yx_oss
description: A pure OSS client library focused on file upload/download with resume support, authentication providers, and minimal dependencies.
version: 1.0.0
homepage: https://github.com/yuanxuan/yx_oss
environment:
sdk: ">=3.0.0 <4.0.0"
flutter: ">=3.0.0"
dependencies:
flutter:
sdk: flutter
dio: ^5.8.0
crypto: ^3.0.3
mime: ^1.0.4
uuid: ^3.0.0
meta: ^1.11.0
path_provider: ^2.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: false

0
test_object_key.dart Normal file
View File