Skip to content

Commit 8b9bccc

Browse files
Merge branch 'develop' into 'fb-LEAP-1840'
Workflow run: https://github.com/HumanSignal/label-studio/actions/runs/14924777069
2 parents d44d0eb + b8e917a commit 8b9bccc

File tree

14 files changed

+130
-77
lines changed

14 files changed

+130
-77
lines changed

label_studio/io_storages/README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,29 @@ The Storage Proxy API behavior can be configured using the following environment
184184
| `RESOLVER_PROXY_MAX_RANGE_SIZE` | Maximum size in bytes for a single range request | 7*1024*1024 |
185185
| `RESOLVER_PROXY_CACHE_TIMEOUT` | Cache TTL in seconds for proxy responses | 3600 |
186186

187-
These optimizations ensure that the Proxy API remains responsive and resource-efficient, even when handling large files or many concurrent requests.
187+
These optimizations ensure that the Proxy API remains responsive and resource-efficient, even when handling large files or many concurrent requests.
188+
189+
## Multiple Storages and URL Resolving
190+
191+
There are use cases where multiple storages can/must be used in a single project. This can cause some confusion as to which storage gets used when. Here are some common cases and how to set up mutliple storages properly.
192+
193+
### Case 1 - Tasks Referencing Other Buckets
194+
* bucket-A containing JSON tasks
195+
* bucket-B containing images/text/other data
196+
* Tasks synced from bucket-A have references to data in bucket-B
197+
198+
##### How To Setup
199+
* Add storage 1 for bucket-A
200+
* Add storage 2 for bucket-B (might be same or different credentials than bucket-A)
201+
* Sync storage 1
202+
* All references to data in bucket-B will be resolved using storage 2 automatically
203+
204+
### Case 2 - Buckets with Different Credentials
205+
* bucket-A accessible by credentials 1
206+
* bucket-B accessible by credentials 2
207+
208+
##### How To Setup
209+
* Add storage 1 for bucket-A with credentials 1
210+
* Add storage 2 for bucket-B with credentials 2
211+
* Sync both storages
212+
* The appropriate storage will be used to resolve urls/generate presigned URLs

label_studio/io_storages/base_models.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from django.utils import timezone
2828
from django.utils.translation import gettext_lazy as _
2929
from django_rq import job
30-
from io_storages.utils import get_uri_via_regex
30+
from io_storages.utils import get_uri_via_regex, parse_bucket_uri
3131
from rq.job import Job
3232
from tasks.models import Annotation, Task
3333
from tasks.serializers import AnnotationSerializer, PredictionSerializer
@@ -255,8 +255,19 @@ def can_resolve_scheme(self, url: Union[str, None]) -> bool:
255255
return False
256256
# TODO: Search for occurrences inside string, e.g. for cases like "gs://bucket/file.pdf" or "<embed src='gs://bucket/file.pdf'/>"
257257
_, prefix = get_uri_via_regex(url, prefixes=(self.url_scheme,))
258-
if prefix == self.url_scheme:
259-
return True
258+
bucket_uri = parse_bucket_uri(url, self)
259+
260+
# If there is a prefix and the bucket matches the storage's bucket/container/path
261+
if prefix == self.url_scheme and bucket_uri:
262+
# bucket is used for s3 and gcs
263+
if hasattr(self, 'bucket') and bucket_uri.bucket == self.bucket:
264+
return True
265+
# container is used for azure blob
266+
if hasattr(self, 'container') and bucket_uri.bucket == self.container:
267+
return True
268+
# path is used for redis
269+
if hasattr(self, 'path') and bucket_uri.bucket == self.path:
270+
return True
260271
# if not found any occurrences - this Storage can't resolve url
261272
return False
262273

label_studio/io_storages/functions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,5 @@ def get_storage_by_url(url: Union[str, List, Dict], storage_objects: Iterable[Im
5454
for storage_object in storage_objects:
5555
if storage_object.can_resolve_url(url):
5656
# note: only first found storage_object will be used for link resolving
57-
# probably we need to use more advanced can_resolve_url mechanics
58-
# that takes into account not only prefixes, but bucket path too
57+
# can_resolve_url now checks both the scheme and the bucket to ensure the correct storage is used
5958
return storage_object

label_studio/io_storages/s3/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def validate(self, data):
6868
except TypeError as e:
6969
logger.info(f'It seems access keys are incorrect: {e}', exc_info=True)
7070
raise ValidationError('It seems access keys are incorrect')
71+
except KeyError:
72+
raise ValidationError(f'{storage.url_scheme}://{storage.bucket}/{storage.prefix} not found.')
7173
return data
7274

7375

label_studio/tasks/models.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,10 @@ def prepare_filename(filename):
420420
def resolve_storage_uri(self, url) -> Optional[Mapping[str, Any]]:
421421
from io_storages.functions import get_storage_by_url
422422

423-
storage = self.storage
424-
project = self.project
425-
426-
if not storage:
427-
storage_objects = project.get_all_import_storage_objects
428-
storage = get_storage_by_url(url, storage_objects)
423+
# Instead of using self.storage, we check all storage objects for the project to
424+
# support imported tasks that point to another bucket
425+
storage_objects = self.project.get_all_import_storage_objects
426+
storage = get_storage_by_url(url, storage_objects)
429427

430428
if storage:
431429
return {
@@ -468,10 +466,9 @@ def resolve_uri(self, task_data, project):
468466

469467
# project storage
470468
# TODO: to resolve nested lists and dicts we should improve get_storage_by_url(),
471-
# TODO: problem with current approach: it can be used only the first storage that get_storage_by_url
472-
# TODO: returns. However, maybe the second storage will resolve uris properly.
473-
# TODO: resolve_uri() already supports them
474-
storage = self.storage or get_storage_by_url(task_data[field], storage_objects)
469+
# Now always using get_storage_by_url to ensure the storage with the correct bucket is used
470+
# As a last fallback we can use self.storage which is the storage the Task was imported from
471+
storage = get_storage_by_url(task_data[field], storage_objects) or self.storage
475472
if storage:
476473
try:
477474
resolved_uri = storage.resolve_uri(task_data[field], self)

web/libs/editor/src/mixins/KonvaRegion.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { types } from "mobx-state-tree";
22
import { FF_DEV_3793, FF_ZOOM_OPTIM, isFF } from "../utils/feature-flags";
3+
import Constants from "../core/Constants";
4+
35
export const KonvaRegionMixin = types
46
.model({})
57
.views((self) => {
@@ -51,6 +53,28 @@ export const KonvaRegionMixin = types
5153
let deferredSelectId = null;
5254

5355
return {
56+
updateCursor(isHovered = false) {
57+
const stage = self.parent?.stageRef;
58+
if (!stage) return;
59+
const style = stage.container().style;
60+
61+
if (isHovered) {
62+
if (self.annotation.isLinkingMode) {
63+
style.cursor = Constants.LINKING_MODE_CURSOR;
64+
} else if (self.type !== "brushregion") {
65+
style.cursor = Constants.POINTER_CURSOR;
66+
}
67+
return;
68+
}
69+
70+
const selectedTool = self.parent?.getToolsManager().findSelectedTool();
71+
if (!selectedTool || !selectedTool.updateCursor) {
72+
style.cursor = Constants.DEFAULT_CURSOR;
73+
} else {
74+
selectedTool.updateCursor();
75+
}
76+
},
77+
5478
checkSizes() {
5579
const { naturalWidth, naturalHeight, stageWidth: width, stageHeight: height } = self.parent;
5680

web/libs/editor/src/regions/BrushRegion.jsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -704,20 +704,14 @@ const HtxBrushView = ({ item, setShapeRef }) => {
704704
onMouseOver={() => {
705705
if (store.annotationStore.selected.isLinkingMode) {
706706
item.setHighlight(true);
707-
stage.container().style.cursor = "crosshair";
708-
} else {
709-
// no tool selected
710-
if (!item.parent.getToolsManager().findSelectedTool()) stage.container().style.cursor = "pointer";
711707
}
708+
item.updateCursor(true);
712709
}}
713710
onMouseOut={() => {
714711
if (store.annotationStore.selected.isLinkingMode) {
715712
item.setHighlight(false);
716713
}
717-
718-
if (!item.parent?.getToolsManager().findSelectedTool()) {
719-
stage.container().style.cursor = "default";
720-
}
714+
item.updateCursor();
721715
}}
722716
onClick={(e) => {
723717
if (item.parent.getSkipInteractions()) return;

web/libs/editor/src/regions/EllipseRegion.jsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,17 +377,14 @@ const HtxEllipseView = ({ item, setShapeRef }) => {
377377
onMouseOver={() => {
378378
if (store.annotationStore.selected.isLinkingMode) {
379379
item.setHighlight(true);
380-
stage.container().style.cursor = Constants.LINKING_MODE_CURSOR;
381-
} else {
382-
stage.container().style.cursor = Constants.POINTER_CURSOR;
383380
}
381+
item.updateCursor(true);
384382
}}
385383
onMouseOut={() => {
386-
stage.container().style.cursor = Constants.DEFAULT_CURSOR;
387-
388384
if (store.annotationStore.selected.isLinkingMode) {
389385
item.setHighlight(false);
390386
}
387+
item.updateCursor();
391388
}}
392389
onClick={(e) => {
393390
if (item.parent.getSkipInteractions()) return;

web/libs/editor/src/regions/KeyPointRegion.jsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,14 @@ const HtxKeyPointView = ({ item, setShapeRef }) => {
258258
onMouseOver={() => {
259259
if (store.annotationStore.selected.isLinkingMode) {
260260
item.setHighlight(true);
261-
stage.container().style.cursor = "crosshair";
262-
} else {
263-
stage.container().style.cursor = "pointer";
264261
}
262+
item.updateCursor(true);
265263
}}
266264
onMouseOut={() => {
267-
stage.container().style.cursor = "default";
268-
269265
if (store.annotationStore.selected.isLinkingMode) {
270266
item.setHighlight(false);
271267
}
268+
item.updateCursor();
272269
}}
273270
onClick={(e) => {
274271
if (item.parent.getSkipInteractions()) return;

web/libs/editor/src/regions/PolygonRegion.jsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -623,17 +623,14 @@ const HtxPolygonView = ({ item, setShapeRef }) => {
623623
onMouseOver={() => {
624624
if (store.annotationStore.selected.isLinkingMode) {
625625
item.setHighlight(true);
626-
stage.container().style.cursor = Constants.LINKING_MODE_CURSOR;
627-
} else {
628-
stage.container().style.cursor = Constants.POINTER_CURSOR;
629626
}
627+
item.updateCursor(true);
630628
}}
631629
onMouseOut={() => {
632-
stage.container().style.cursor = Constants.DEFAULT_CURSOR;
633-
634630
if (store.annotationStore.selected.isLinkingMode) {
635631
item.setHighlight(false);
636632
}
633+
item.updateCursor();
637634
}}
638635
onClick={(e) => {
639636
// create regions over another regions with Cmd/Ctrl pressed

web/libs/editor/src/regions/RectRegion.jsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -499,17 +499,14 @@ const HtxRectangleView = ({ item, setShapeRef }) => {
499499
onMouseOver={() => {
500500
if (store.annotationStore.selected.isLinkingMode) {
501501
item.setHighlight(true);
502-
stage.container().style.cursor = Constants.LINKING_MODE_CURSOR;
503-
} else {
504-
stage.container().style.cursor = Constants.POINTER_CURSOR;
505502
}
503+
item.updateCursor(true);
506504
}}
507505
onMouseOut={() => {
508-
stage.container().style.cursor = Constants.DEFAULT_CURSOR;
509-
510506
if (store.annotationStore.selected.isLinkingMode) {
511507
item.setHighlight(false);
512508
}
509+
item.updateCursor();
513510
}}
514511
onClick={(e) => {
515512
if (item.parent.getSkipInteractions()) return;

web/libs/editor/src/tools/Brush.jsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,9 @@ const _Tool = types
124124
return newArea;
125125
},
126126

127-
updateCursor() {
128-
if (!self.selected || !self.obj?.stageRef) return;
129-
const val = self.strokeWidth;
130-
const stage = self.obj.stageRef;
131-
const base64 = Canvas.brushSizeCircle(val);
132-
const cursor = ["url('", base64, "')", " ", Math.floor(val / 2) + 4, " ", Math.floor(val / 2) + 4, ", auto"];
133-
134-
stage.container().style.cursor = cursor.join("");
135-
},
136-
137127
setStroke(val) {
138128
self.strokeWidth = val;
129+
self.updateCursor();
139130
},
140131

141132
afterUpdateSelected() {
@@ -236,6 +227,22 @@ const _Tool = types
236227
};
237228
});
238229

239-
const Brush = types.compose(_Tool.name, ToolMixin, BaseTool, DrawingTool, _Tool);
230+
const BrushCursorMixin = types
231+
.model("BrushCursorMixin")
232+
.views((self) => ({
233+
get cursorStyleRule() {
234+
const val = self.strokeWidth;
235+
return Canvas.createBrushSizeCircleCursor(val);
236+
},
237+
}))
238+
.actions((self) => ({
239+
updateCursor() {
240+
if (!self.selected || !self.obj?.stageRef) return;
241+
const stage = self.obj.stageRef;
242+
stage.container().style.cursor = self.cursorStyleRule;
243+
},
244+
}));
245+
246+
const Brush = types.compose(_Tool.name, ToolMixin, BaseTool, DrawingTool, BrushCursorMixin, _Tool);
240247

241-
export { Brush };
248+
export { Brush, BrushCursorMixin };

web/libs/editor/src/tools/Erase.jsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { types } from "mobx-state-tree";
33

44
import BaseTool from "./Base";
55
import ToolMixin from "../mixins/Tool";
6-
import Canvas from "../utils/canvas";
76
import { clamp, findClosestParent } from "../utils/utilities";
87
import { DrawingTool } from "../mixins/DrawingTool";
98
import { IconEraserTool } from "@humansignal/icons";
109
import { Tool } from "../components/Toolbar/Tool";
1110
import { Range } from "../common/Range/Range";
11+
import { BrushCursorMixin } from "./Brush";
1212

1313
const MIN_SIZE = 1;
1414
const MAX_SIZE = 50;
@@ -103,16 +103,6 @@ const _Tool = types
103103
let brush;
104104

105105
return {
106-
updateCursor() {
107-
if (!self.selected || !self.obj?.stageRef) return;
108-
const val = 24;
109-
const stage = self.obj.stageRef;
110-
const base64 = Canvas.brushSizeCircle(val);
111-
const cursor = ["url('", base64, "')", " ", Math.floor(val / 2) + 4, " ", Math.floor(val / 2) + 4, ", auto"];
112-
113-
stage.container().style.cursor = cursor.join("");
114-
},
115-
116106
afterUpdateSelected() {
117107
self.updateCursor();
118108
},
@@ -123,6 +113,7 @@ const _Tool = types
123113

124114
setStroke(val) {
125115
self.strokeWidth = val;
116+
self.updateCursor();
126117
},
127118

128119
mouseupEv() {
@@ -174,6 +165,6 @@ const _Tool = types
174165
};
175166
});
176167

177-
const Erase = types.compose(_Tool.name, ToolMixin, BaseTool, DrawingTool, _Tool);
168+
const Erase = types.compose(_Tool.name, ToolMixin, BaseTool, DrawingTool, BrushCursorMixin, _Tool);
178169

179170
export { Erase };

0 commit comments

Comments
 (0)