Merge pull request #15809 from mvdbeek/dev

Merge 23.0 into dev
This commit is contained in:
Marius van den Beek
2023-03-16 10:03:05 +01:00
committed by GitHub
27 changed files with 535 additions and 393 deletions
+1 -1
View File
@@ -187,7 +187,7 @@
"vue-template-compiler": "^2.7.14",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1",
"webpack-dev-server": "^4.12.0",
"webpack-merge": "^5.8.0",
"yaml-jest": "^1.2.0",
"yaml-loader": "^0.8.0"
@@ -4,10 +4,16 @@ import { BAlert, BCard, BCardTitle } from "bootstrap-vue";
import LoadingSpan from "components/LoadingSpan";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock } from "@fortawesome/free-solid-svg-icons";
import {
faExclamationCircle,
faExclamationTriangle,
faCheckCircle,
faClock,
faLink,
} from "@fortawesome/free-solid-svg-icons";
import { ExportRecordModel } from "./models/exportRecordModel";
library.add(faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock);
library.add(faExclamationCircle, faExclamationTriangle, faCheckCircle, faClock, faLink);
const props = defineProps({
record: {
@@ -28,7 +34,7 @@ const props = defineProps({
},
});
const emit = defineEmits(["onReimport", "onDownload", "onActionMessageDismissed"]);
const emit = defineEmits(["onReimport", "onDownload", "onCopyDownloadLink", "onActionMessageDismissed"]);
const title = computed(() => (props.record.isReady ? `Exported` : `Export started`));
const preparingMessage = computed(
@@ -43,6 +49,10 @@ function downloadObject() {
emit("onDownload", props.record);
}
function copyDownloadLink() {
emit("onCopyDownloadLink", props.record);
}
function onMessageDismissed() {
emit("onActionMessageDismissed");
}
@@ -113,6 +123,14 @@ function onMessageDismissed() {
@click="downloadObject">
Download
</b-button>
<b-button
v-if="props.record.canDownload"
title="Copy Download Link"
size="sm"
variant="link"
@click.stop="copyDownloadLink">
<font-awesome-icon icon="link" />
</b-button>
<b-button
v-if="props.record.canReimport"
class="record-reimport-btn"
@@ -9,9 +9,10 @@ import {
faDownload,
faFileImport,
faSpinner,
faLink,
} from "@fortawesome/free-solid-svg-icons";
library.add(faExclamationCircle, faCheckCircle, faDownload, faFileImport, faSpinner);
library.add(faExclamationCircle, faCheckCircle, faDownload, faFileImport, faSpinner, faLink);
const props = defineProps({
records: {
@@ -32,7 +33,7 @@ const fields = [
];
const isExpanded = ref(false);
const title = computed(() => (isExpanded.value ? `Hide export records` : `Show export records`));
const title = computed(() => (isExpanded.value ? `Hide old export records` : `Show old export records`));
async function reimportObject(record) {
emit("onReimport", record);
@@ -41,6 +42,10 @@ async function reimportObject(record) {
function downloadObject(record) {
emit("onDownload", record);
}
function copyDownloadLink(record) {
emit("onCopyDownloadLink", record);
}
</script>
<template>
@@ -113,6 +118,12 @@ function downloadObject(record) {
@click="downloadObject(row.item)">
<font-awesome-icon icon="download" />
</b-button>
<b-button
v-if="row.item.canDownload"
title="Copy Download Link"
@click.stop="copyDownloadLink(row.item)">
<font-awesome-icon icon="link" />
</b-button>
<b-button
v-b-tooltip.hover.bottom
:disabled="!row.item.canReimport"
@@ -11,6 +11,8 @@ import { useTaskMonitor } from "composables/taskMonitor";
import { useFileSources } from "composables/fileSources";
import { useShortTermStorage, DEFAULT_EXPORT_PARAMS } from "composables/shortTermStorage";
import { useConfirmDialog } from "composables/confirmDialog";
import { copy as sendToClipboard } from "utils/clipboard";
import { absPath } from "@/utils/redirect";
const {
isRunning: isExportTaskRunning,
@@ -18,8 +20,16 @@ const {
requestHasFailed: taskMonitorRequestFailed,
hasFailed: taskHasFailed,
} = useTaskMonitor();
const { hasWritable: hasWritableFileSources } = useFileSources();
const { isPreparing: isPreparingDownload, downloadHistory, downloadObjectByRequestId } = useShortTermStorage();
const {
isPreparing: isPreparingDownload,
downloadHistory,
downloadObjectByRequestId,
getDownloadObjectUrl,
} = useShortTermStorage();
const { confirm } = useConfirmDialog();
const props = defineProps({
@@ -104,6 +114,13 @@ function downloadFromRecord(record) {
}
}
function copyDownloadLinkFromRecord(record) {
if (record.canDownload) {
const relativeLink = getDownloadObjectUrl(record.stsDownloadId);
sendToClipboard(absPath(relativeLink), "Download link copied to your clipboard");
}
}
function findValidUpToDateDownloadRecord() {
return exportRecords.value
? exportRecords.value.find(
@@ -203,6 +220,7 @@ function updateExportParams(newParams) {
:action-message="actionMessage"
:action-message-variant="actionMessageVariant"
@onDownload="downloadFromRecord"
@onCopyDownloadLink="copyDownloadLinkFromRecord"
@onReimport="reimportFromRecord"
@onActionMessageDismissed="onActionMessageDismissedFromRecord" />
<b-alert v-else id="no-export-records-alert" variant="info" class="mt-3" show>
@@ -110,7 +110,7 @@ export default {
this.$emit("onResults", this.favoritesResults);
} else {
// keys with sorting order
const keys = { exact: 3, name: 2, description: 1, combined: 0 };
const keys = { exact: 4, name: 3, hyphenated: 2, description: 1, combined: 0 };
this.$emit("onResults", searchToolsByKeys(this.toolsList, keys, q));
}
} else {
@@ -92,6 +92,8 @@ export function searchToolsByKeys(tools, keys, query) {
let actualValue = "";
if (key === "combined") {
actualValue = tool.name.toLowerCase() + " " + tool.description.toLowerCase();
} else if (key === "hyphenated") {
actualValue = tool.name.toLowerCase().replaceAll("-", " ");
} else {
actualValue = tool[key] ? tool[key].toLowerCase() : "";
}
@@ -44,6 +44,28 @@ describe("test helpers in tool searching utilities", () => {
keys = { description: 1, name: 2, combined: 0 };
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
expect(results).toEqual(expectedResults);
const tempToolsList = [
{
elems: [
{
panel_section_name: "FASTA/FASTQ",
description: "Extract UMI from fastq files",
id: "toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2",
name: "UMI-tools extract",
},
],
model_class: "ToolSection",
id: "fasta/fastq",
name: "FASTA/FASTQ",
},
];
// hyphenated tool-name is searchable
q = "uMi tools extract ";
expectedResults = ["toolshed.g2.bx.psu.edu/repos/iuc/umi_tools_extract/umi_tools_extract/1.1.2+galaxy2"];
keys = { description: 1, name: 2, hyphenated: 0 };
results = searchToolsByKeys(normalizeTools(tempToolsList), keys, q);
expect(results).toEqual(expectedResults);
});
it("test tool filtering helpers on toolsList given list of ids", async () => {
@@ -656,6 +656,7 @@ export default {
getModule(stepData, id, this.stateStore.setLoadingState).then((response) => {
this.stepStore.updateStep({
...stepData,
id: id,
tool_state: response.tool_state,
inputs: response.inputs,
outputs: response.outputs,
@@ -81,7 +81,6 @@
:root-offset="rootOffset"
:scroll="scroll"
:scale="scale"
v-on="$listeners"
@onChange="onChange" />
<div v-if="showRule" class="rule" />
<node-output
@@ -97,7 +96,7 @@
:scroll="scroll"
:scale="scale"
:datatypes-mapper="datatypesMapper"
v-on="$listeners"
@onDragConnector="onDragConnector"
@stopDragging="onStopDragging"
@onChange="onChange" />
</div>
@@ -117,7 +116,7 @@ import NodeOutput from "@/components/Workflow/Editor/NodeOutput.vue";
import DraggableWrapper from "@/components/Workflow/Editor/DraggablePan.vue";
import { computed, ref } from "vue";
import { useNodePosition } from "@/components/Workflow/Editor/composables/useNodePosition";
import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore";
import { useWorkflowStateStore, type TerminalPosition, type XYPosition } from "@/stores/workflowEditorStateStore";
import type { Step } from "@/stores/workflowStepStore";
import { DatatypesMapperModel } from "@/components/Datatypes/model";
import type { UseElementBoundingReturn, UseScrollReturn } from "@vueuse/core";
@@ -126,6 +125,7 @@ import { useWorkflowStepStore } from "@/stores/workflowStepStore";
import { faCodeBranch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import type { OutputTerminals } from "./modules/terminals";
Vue.use(BootstrapVue);
@@ -158,6 +158,7 @@ const emit = defineEmits([
"onClone",
"onUpdateStepPosition",
"pan-by",
"onDragConnector",
"stopDragging",
]);
@@ -240,6 +241,10 @@ const outputs = computed(() => {
return [...stepOutputs, ...invalidOutputs.value];
});
function onDragConnector(dragPosition: TerminalPosition, terminal: OutputTerminals) {
emit("onDragConnector", dragPosition, terminal);
}
function onMoveTo(position: XYPosition) {
emit("onUpdateStepPosition", props.id, {
top: position.y + props.scroll.y.value / props.scale,
@@ -196,9 +196,6 @@ export default {
dragLeave(event) {
this.$root.$emit("bv::hide::tooltip", this.iconId);
},
onChange() {
this.$emit("onChange");
},
onRemove() {
const connections = this.connectionStore.getConnectionsForTerminal(this.id);
connections.forEach((connection) => this.terminal.disconnect(connection));
@@ -1,9 +1,220 @@
<script setup lang="ts">
import DraggableWrapper from "./DraggablePan.vue";
import { useCoordinatePosition, type ElementBounding } from "./composables/useCoordinatePosition";
import { useTerminal } from "./composables/useTerminal";
import { ref, computed, watch, nextTick, toRefs, onBeforeUnmount, type Ref } from "vue";
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore";
import ConnectionMenu from "@/components/Workflow/Editor/ConnectionMenu.vue";
import {
useWorkflowStepStore,
type OutputTerminalSource,
type Step,
type PostJobActions,
type PostJobAction,
} from "@/stores/workflowStepStore";
import { assertDefined } from "@/utils/assertions";
import type { UseScrollReturn } from "@vueuse/core";
const props = defineProps<{
output: OutputTerminalSource;
workflowOutputs: NonNullable<Step["workflow_outputs"]>;
stepType: Step["type"];
stepId: number;
postJobActions: PostJobActions;
stepPosition: NonNullable<Step["position"]>;
rootOffset: Ref<ElementBounding>;
scroll: UseScrollReturn;
scale: number;
datatypesMapper: DatatypesMapperModel;
}>();
const emit = defineEmits(["pan-by", "stopDragging", "onDragConnector"]);
const stateStore = useWorkflowStateStore();
const stepStore = useWorkflowStepStore();
const el = ref(null);
const { rootOffset, stepPosition, output, stepId, datatypesMapper } = toRefs(props);
const position = useCoordinatePosition(el, rootOffset, stepPosition);
const extensions = computed(() => {
let changeDatatype: PostJobAction | undefined;
if ("label" in props.output && props.postJobActions[`ChangeDatatypeAction${props.output.label}`]) {
changeDatatype = props.postJobActions[`ChangeDatatypeAction${props.output.label}`];
} else {
changeDatatype = props.postJobActions[`ChangeDatatypeAction${props.output.name}`];
}
let extensions =
changeDatatype?.action_arguments.newtype ||
("extensions" in props.output && props.output.extensions) ||
("type" in props.output && props.output.type) ||
"unspecified";
if (!Array.isArray(extensions)) {
extensions = [extensions];
}
return extensions;
});
const effectiveOutput = ref({ ...output.value, extensions: extensions.value });
watch(extensions, () => {
effectiveOutput.value = { ...output.value, extensions: extensions.value };
});
const { terminal, isMappedOver: isMultiple } = useTerminal(stepId, effectiveOutput, datatypesMapper);
const workflowOutput = computed(() =>
props.workflowOutputs.find((workflowOutput) => workflowOutput.output_name == props.output.name)
);
const activeClass = computed(() => workflowOutput.value && "mark-terminal-active");
const isVisible = computed(() => {
const isHidden = `HideDatasetAction${props.output.name}` in props.postJobActions;
return !isHidden;
});
const visibleClass = computed(() => (isVisible.value ? "mark-terminal-visible" : "mark-terminal-hidden"));
const visibleHint = computed(() => {
if (isVisible.value) {
return `Output will be visible in history. Click to hide output.`;
} else {
return `Output will be hidden in history. Click to make output visible.`;
}
});
const label = computed(() => {
const activeLabel = workflowOutput.value?.label || props.output.name;
return `${activeLabel} (${extensions.value.join(", ")})`;
});
const rowClass = computed(() => {
const classes = ["form-row", "dataRow", "output-data-row"];
if ("valid" in props.output && props.output?.valid === false) {
classes.push("form-row-error");
}
return classes;
});
const menu: Ref<InstanceType<typeof ConnectionMenu> | undefined> = ref();
const icon: Ref<HTMLElement | undefined> = ref();
const showChildComponent = ref(false);
function closeMenu() {
showChildComponent.value = false;
}
async function toggleChildComponent() {
showChildComponent.value = !showChildComponent.value;
if (showChildComponent.value) {
await nextTick();
if (menu.value?.$el instanceof HTMLElement) {
menu.value!.$el.focus();
}
} else {
icon.value!.focus();
}
}
function onToggleActive() {
const step = stepStore.getStep(stepId.value);
assertDefined(step);
let stepWorkflowOutputs = [...(step.workflow_outputs || [])];
if (workflowOutput.value) {
stepWorkflowOutputs = stepWorkflowOutputs.filter(
(workflowOutput) => workflowOutput.output_name !== output.value.name
);
} else {
stepWorkflowOutputs.push({ output_name: output.value.name });
}
stepStore.updateStep({ ...step, workflow_outputs: stepWorkflowOutputs });
}
function onToggleVisible() {
const actionKey = `HideDatasetAction${props.output.name}`;
const step = stepStore.getStep(stepId.value);
assertDefined(step);
if (isVisible.value) {
step.post_job_actions = {
...step.post_job_actions,
[actionKey]: {
action_type: "HideDatasetAction",
output_name: props.output.name,
action_arguments: {},
},
};
} else {
if (step.post_job_actions) {
const { [actionKey]: ignoreUnused, ...newPostJobActions } = step.post_job_actions;
step.post_job_actions = newPostJobActions;
} else {
step.post_job_actions = {};
}
}
stepStore.updateStep(step);
}
function onPanBy(panBy: XYPosition) {
emit("pan-by", panBy);
}
function onStopDragging() {
isDragging.value = false;
dragX.value = 0;
dragY.value = 0;
emit("stopDragging");
}
const dragX = ref(0);
const dragY = ref(0);
const isDragging = ref(false);
const startX = computed(() => position.left + props.scroll.x.value / props.scale + position.width / 2);
const startY = computed(() => position.top + props.scroll.y.value / props.scale + position.height / 2);
const endX = computed(() => {
return (dragX.value || startX.value) + props.scroll.x.value / props.scale;
});
const endY = computed(() => {
return (dragY.value || startY.value) + props.scroll.y.value / props.scale;
});
const dragPosition = computed(() => {
return {
startX: startX.value,
endX: endX.value,
startY: startY.value,
endY: endY.value,
};
});
const terminalPosition = computed(() => {
return Object.freeze({ startX: startX.value, startY: startY.value });
});
watch([dragPosition, isDragging], () => {
if (isDragging.value) {
emit("onDragConnector", dragPosition.value, terminal.value);
}
});
watch(terminalPosition, () =>
stateStore.setOutputTerminalPosition(props.stepId, props.output.name, terminalPosition.value)
);
function onMove(dragPosition: XYPosition) {
dragX.value = dragPosition.x + position.width / 2;
dragY.value = dragPosition.y + position.height / 2;
}
const id = computed(() => `node-${props.stepId}-output-${props.output.name}`);
const showCalloutActiveOutput = computed(() => props.stepType === "tool" || props.stepType === "subworkflow");
const showCalloutVisible = computed(() => props.stepType === "tool");
const terminalClass = computed(() => {
const cls = "terminal output-terminal";
if (isMultiple.value) {
return `${cls} multiple`;
}
return cls;
});
onBeforeUnmount(() => {
stateStore.deleteOutputTerminalPosition(props.stepId, props.output.name);
});
</script>
<template>
<div :class="rowClass">
<div :class="rowClass" :data-output-name="output.name">
<div
v-if="showCalloutActiveOutput"
v-b-tooltip
:class="['callout-terminal', output.name]"
class="callout-terminal"
title="Checked outputs will become primary workflow outputs and are available as subworkflow outputs."
@keyup="onToggleActive"
@click="onToggleActive">
@@ -12,7 +223,7 @@
<div
v-if="showCalloutVisible"
v-b-tooltip
:class="['callout-terminal', output.name]"
class="callout-terminal"
:title="visibleHint"
@keyup="onToggleVisible"
@click="onToggleVisible">
@@ -50,267 +261,3 @@
</draggable-wrapper>
</div>
</template>
<script>
import DraggableWrapper from "./DraggablePan";
import { useCoordinatePosition } from "./composables/useCoordinatePosition";
import { useTerminal } from "./composables/useTerminal";
import { ref, computed, watch, nextTick, toRefs } from "vue";
import { DatatypesMapperModel } from "@/components/Datatypes/model";
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
import ConnectionMenu from "@/components/Workflow/Editor/ConnectionMenu";
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
export default {
components: {
ConnectionMenu,
DraggableWrapper,
},
props: {
output: {
type: Object,
required: true,
},
workflowOutputs: {
type: Array,
required: true,
},
stepType: {
type: String,
required: true,
},
stepId: {
type: Number,
required: true,
},
postJobActions: {
type: Object,
required: true,
},
stepPosition: {
type: Object,
required: true,
},
rootOffset: {
type: Object,
required: true,
},
scroll: {
type: Object,
required: true,
},
scale: {
type: Number,
required: true,
},
datatypesMapper: {
type: DatatypesMapperModel,
required: true,
},
},
setup(props) {
const stateStore = useWorkflowStateStore();
const stepStore = useWorkflowStepStore();
const el = ref(null);
const { rootOffset, stepPosition, output, stepId, datatypesMapper } = toRefs(props);
const position = useCoordinatePosition(el, rootOffset, stepPosition);
const extensions = computed(() => {
const changeDatatype =
props.postJobActions[`ChangeDatatypeAction${props.output.label}`] ||
props.postJobActions[`ChangeDatatypeAction${props.output.name}`];
let extensions =
changeDatatype?.action_arguments.newtype ||
props.output.extensions ||
props.output.type ||
"unspecified";
if (!Array.isArray(extensions)) {
extensions = [extensions];
}
return extensions;
});
const effectiveOutput = ref({ ...output.value, extensions: extensions.value });
watch(extensions, () => {
effectiveOutput.value = { ...output.value, extensions: extensions.value };
});
const { terminal, isMappedOver: isMultiple } = useTerminal(stepId, effectiveOutput, datatypesMapper);
const workflowOutput = computed(() =>
props.workflowOutputs.find((workflowOutput) => workflowOutput.output_name == props.output.name)
);
const activeClass = computed(() => workflowOutput.value && "mark-terminal-active");
const isVisible = computed(() => {
const isHidden = `HideDatasetAction${props.output.name}` in props.postJobActions;
return !isHidden;
});
const visibleClass = computed(() => (isVisible.value ? "mark-terminal-visible" : "mark-terminal-hidden"));
const visibleHint = computed(() => {
if (isVisible.value) {
return `Output will be visible in history. Click to hide output.`;
} else {
return `Output will be hidden in history. Click to make output visible.`;
}
});
const label = computed(() => {
const activeLabel = workflowOutput.value?.label || props.output.name;
return `${activeLabel} (${extensions.value.join(", ")})`;
});
const rowClass = computed(() => {
const classes = ["form-row", "dataRow", "output-data-row"];
if (props.output?.valid === false) {
classes.push("form-row-error");
}
return classes;
});
const menu = ref(null);
const icon = ref(null);
const showChildComponent = ref(false);
function closeMenu() {
showChildComponent.value = false;
}
async function toggleChildComponent() {
showChildComponent.value = !showChildComponent.value;
if (showChildComponent.value) {
await nextTick();
menu.value.$el.focus();
} else {
icon.value.focus();
}
}
function onToggleActive() {
const step = stepStore.getStep(stepId.value);
if (workflowOutput.value) {
step.workflow_outputs = step.workflow_outputs.filter(
(workflowOutput) => workflowOutput.output_name !== output.value.name
);
} else {
step.workflow_outputs.push({ output_name: output.value.name });
}
stepStore.updateStep(step);
}
function onToggleVisible() {
const actionKey = `HideDatasetAction${props.output.name}`;
const step = stepStore.getStep(stepId.value);
if (isVisible.value) {
step.post_job_actions = {
...step.post_job_actions,
[actionKey]: {
action_type: "HideDatasetAction",
output_name: props.output.name,
action_arguments: {},
},
};
} else {
const { [actionKey]: ignoreUnused, ...newPostJobActions } = step.post_job_actions;
step.post_job_actions = newPostJobActions;
}
stepStore.updateStep(step);
}
return {
el,
icon,
position,
activeClass,
visibleClass,
visibleHint,
rowClass,
isVisible,
terminal,
isMultiple,
label,
stateStore,
menu,
showChildComponent,
toggleChildComponent,
closeMenu,
effectiveOutput,
onToggleActive,
onToggleVisible,
};
},
data() {
return {
isDragging: false,
dragX: 0,
dragY: 0,
};
},
computed: {
terminalPosition() {
return Object.freeze({ startX: this.startX, startY: this.startY });
},
startX() {
return this.position.left + this.scroll.x.value / this.scale + this.position.width / 2;
},
startY() {
return this.position.top + this.scroll.y.value / this.scale + this.position.height / 2;
},
endX() {
return (this.dragX || this.startX) + this.scroll.x.value / this.scale;
},
endY() {
return (this.dragY || this.startY) + this.scroll.y.value / this.scale;
},
dragPosition() {
return {
startX: this.startX,
endX: this.endX,
startY: this.startY,
endY: this.endY,
};
},
id() {
return `node-${this.stepId}-output-${this.output.name}`;
},
showCalloutActiveOutput() {
return this.stepType === "tool" || this.stepType === "subworkflow";
},
showCalloutVisible() {
return this.stepType === "tool";
},
terminalClass() {
const cls = "terminal output-terminal";
if (this.isMultiple) {
return `${cls} multiple`;
}
return cls;
},
},
watch: {
terminalPosition(position) {
this.stateStore.setOutputTerminalPosition(this.stepId, this.output.name, position);
},
dragPosition() {
if (this.isDragging) {
this.$emit("onDragConnector", this.dragPosition, this.terminal);
}
},
},
beforeDestroy() {
this.stateStore.deleteOutputTerminalPosition({
stepId: this.stepId,
outputName: this.output.name,
});
},
methods: {
onPanBy(panBy) {
this.$emit("pan-by", panBy);
},
onMove(position, event) {
this.dragX = position.x + this.position.width / 2;
this.dragY = position.y + this.position.height / 2;
},
onStopDragging(e) {
this.isDragging = false;
this.dragX = 0;
this.dragY = 0;
this.$emit("stopDragging");
},
},
};
</script>
+11 -1
View File
@@ -30,8 +30,13 @@ export function useShortTermStorage() {
return prepareObjectDownload(invocationId, "invocations", options);
}
function downloadObjectByRequestId(storageRequestId) {
function getDownloadObjectUrl(storageRequestId) {
const url = withPrefix(`/api/short_term_storage/${storageRequestId}`);
return url;
}
function downloadObjectByRequestId(storageRequestId) {
const url = getDownloadObjectUrl(storageRequestId);
window.location.assign(url);
}
@@ -119,5 +124,10 @@ export function useShortTermStorage() {
* Whether the download is still being prepared.
*/
isPreparing: readonly(isPreparing),
/**
* Given a storageRequestId it returns the download URL for that object.
* @param {String} storageRequestId The storage request ID associated to the object to be downloaded
*/
getDownloadObjectUrl,
};
}
+50 -54
View File
@@ -1,5 +1,6 @@
import { defineStore } from "pinia";
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
import { pushOrSet } from "@/utils/pushOrSet";
import Vue from "vue";
interface InvalidConnections {
@@ -9,6 +10,9 @@ interface InvalidConnections {
export interface State {
connections: Connection[];
invalidConnections: InvalidConnections;
inputTerminalToOutputTerminals: TerminalToOutputTerminals;
terminalToConnection: { [index: string]: Connection[] };
stepToConnections: { [index: number]: Connection[] };
}
export class Connection {
@@ -44,80 +48,38 @@ interface TerminalToOutputTerminals {
[index: string]: OutputTerminal[];
}
interface TerminalToInputTerminals {
[index: string]: InputTerminal[];
}
/**
* Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
* @param object Object which contains array
* @param key Key which array is in
* @param value Value to push
*/
function pushOrSet<T>(object: { [key: string | number]: Array<T> }, key: string | number, value: T) {
if (key in object) {
object[key]!.push(value);
} else {
object[key] = [value];
}
}
export const useConnectionStore = defineStore("workflowConnectionStore", {
state: (): State => ({
connections: [] as Connection[],
invalidConnections: {} as InvalidConnections,
inputTerminalToOutputTerminals: {} as TerminalToOutputTerminals,
terminalToConnection: {} as { [index: string]: Connection[] },
stepToConnections: {} as { [index: number]: Connection[] },
}),
getters: {
getOutputTerminalsForInputTerminal(state: State) {
const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
state.connections.map((connection) => {
const terminals = getTerminals(connection);
const inputTerminalId = getTerminalId(terminals.input);
pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
});
return (terminalId: string): OutputTerminal[] => {
return inputTerminalToOutputTerminals[terminalId] || [];
};
},
getInputTerminalsForOutputTerminal(state: State) {
const outputTerminalToInputTerminals: TerminalToInputTerminals = {};
state.connections.map((connection) => {
const terminals = getTerminals(connection);
const outputTerminalId = getTerminalId(terminals.output);
pushOrSet(outputTerminalToInputTerminals, outputTerminalId, terminals.input);
});
return (terminalId: string): BaseTerminal[] => {
return outputTerminalToInputTerminals[terminalId] || [];
return state.inputTerminalToOutputTerminals[terminalId] || [];
};
},
getConnectionsForTerminal(state: State) {
const terminalToConnection: { [index: string]: Connection[] } = {};
state.connections.map((connection) => {
const terminals = getTerminals(connection);
const outputTerminalId = getTerminalId(terminals.output);
pushOrSet(terminalToConnection, outputTerminalId, connection);
const inputTerminalId = getTerminalId(terminals.input);
pushOrSet(terminalToConnection, inputTerminalId, connection);
});
return (terminalId: string): Connection[] => {
return terminalToConnection[terminalId] || [];
return state.terminalToConnection[terminalId] || [];
};
},
getConnectionsForStep(state: State) {
const stepToConnections: { [index: number]: Connection[] } = {};
state.connections.map((connection) => {
pushOrSet(stepToConnections, connection.input.stepId, connection);
pushOrSet(stepToConnections, connection.output.stepId, connection);
});
return (stepId: number): Connection[] => stepToConnections[stepId] || [];
return (stepId: number): Connection[] => state.stepToConnections[stepId] || [];
},
},
actions: {
addConnection(this: State, connection: Connection) {
addConnection(this, _connection: Connection) {
const connection = Object.freeze(_connection);
this.connections.push(connection);
const stepStore = useWorkflowStepStore();
stepStore.addConnection(connection);
this.terminalToConnection = updateTerminalToConnection(this.connections);
this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
this.stepToConnections = updateStepToConnections(this.connections);
},
markInvalidConnection(this: State, connectionId: string, reason: string) {
Vue.set(this.invalidConnections, connectionId, reason);
@@ -125,7 +87,7 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
dropFromInvalidConnections(this: State, connectionId: string) {
Vue.delete(this.invalidConnections, connectionId);
},
removeConnection(this: State, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
removeConnection(this, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
const stepStore = useWorkflowStepStore();
this.connections = this.connections.filter((connection) => {
if (typeof terminal === "string") {
@@ -154,10 +116,44 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
}
}
});
this.terminalToConnection = updateTerminalToConnection(this.connections);
this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
this.stepToConnections = updateStepToConnections(this.connections);
},
},
});
function updateTerminalToTerminal(connections: Connection[]) {
const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
connections.map((connection) => {
const terminals = getTerminals(connection);
const inputTerminalId = getTerminalId(terminals.input);
pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
});
return inputTerminalToOutputTerminals;
}
function updateTerminalToConnection(connections: Connection[]) {
const terminalToConnection: { [index: string]: Connection[] } = {};
connections.map((connection) => {
const terminals = getTerminals(connection);
const outputTerminalId = getTerminalId(terminals.output);
pushOrSet(terminalToConnection, outputTerminalId, connection);
const inputTerminalId = getTerminalId(terminals.input);
pushOrSet(terminalToConnection, inputTerminalId, connection);
});
return terminalToConnection;
}
function updateStepToConnections(connections: Connection[]) {
const stepToConnections: { [index: number]: Connection[] } = {};
connections.map((connection) => {
pushOrSet(stepToConnections, connection.input.stepId, connection);
pushOrSet(stepToConnections, connection.output.stepId, connection);
});
return stepToConnections;
}
export function getTerminalId(item: BaseTerminal): string {
return `node-${item.stepId}-${item.connectorType}-${item.name}`;
}
+14 -4
View File
@@ -4,6 +4,16 @@ import { defineStore } from "pinia";
import type { OutputTerminals } from "@/components/Workflow/Editor/modules/terminals";
import type { UseElementBoundingReturn } from "@vueuse/core";
export interface InputTerminalPosition {
endX: number;
endY: number;
}
export interface OutputTerminalPosition {
startX: number;
startY: number;
}
export interface TerminalPosition {
startX: number;
endX: number;
@@ -17,8 +27,8 @@ export interface XYPosition {
}
interface State {
inputTerminals: { [index: number]: { [index: string]: TerminalPosition } };
outputTerminals: { [index: number]: { [index: string]: TerminalPosition } };
inputTerminals: { [index: number]: { [index: string]: InputTerminalPosition } };
outputTerminals: { [index: number]: { [index: string]: OutputTerminalPosition } };
draggingPosition: TerminalPosition | null;
draggingTerminal: OutputTerminals | null;
activeNodeId: number | null;
@@ -50,14 +60,14 @@ export const useWorkflowStateStore = defineStore("workflowStateStore", {
},
},
actions: {
setInputTerminalPosition(stepId: number, inputName: string, position: TerminalPosition) {
setInputTerminalPosition(stepId: number, inputName: string, position: InputTerminalPosition) {
if (this.inputTerminals[stepId]) {
Vue.set(this.inputTerminals[stepId]!, inputName, position);
} else {
Vue.set(this.inputTerminals, stepId, { [inputName]: position });
}
},
setOutputTerminalPosition(stepId: number, outputName: string, position: TerminalPosition) {
setOutputTerminalPosition(stepId: number, outputName: string, position: OutputTerminalPosition) {
if (this.outputTerminals[stepId]) {
Vue.set(this.outputTerminals[stepId]!, outputName, position);
} else {
+2 -1
View File
@@ -12,6 +12,7 @@ const stepInputConnection: StepInputConnection = {
};
const workflowStepZero: NewStep = {
id: 0,
input_connections: {},
inputs: [],
name: "a step",
@@ -33,7 +34,7 @@ describe("Connection Store", () => {
const stepStore = useWorkflowStepStore();
expect(stepStore.steps).toStrictEqual({});
stepStore.addStep(workflowStepZero);
expect(stepStore.getStep(0)).toBe(workflowStepZero);
expect(stepStore.getStep(0)).toStrictEqual(workflowStepZero);
expect(workflowStepZero.id).toBe(0);
});
it("removes step", () => {
+31 -29
View File
@@ -10,6 +10,7 @@ interface State {
stepIndex: number;
stepMapOver: { [index: number]: CollectionTypeDescriptor };
stepInputMapOver: StepInputMapOver;
stepExtraInputs: { [index: number]: InputTerminalSource[] };
}
interface StepPosition {
@@ -130,7 +131,7 @@ export interface ConnectionOutputLink {
input_subworkflow_step_id?: number;
}
interface WorkflowOutputs {
export interface WorkflowOutputs {
[index: string]: {
stepId: number;
outputName: string;
@@ -147,6 +148,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
stepMapOver: {} as { [index: number]: CollectionTypeDescriptor },
stepInputMapOver: {} as StepInputMapOver,
stepIndex: -1,
stepExtraInputs: {} as { [index: number]: InputTerminalSource[] },
}),
getters: {
getStep(state: State) {
@@ -155,30 +157,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
};
},
getStepExtraInputs(state: State) {
const extraInputs: { [index: number]: InputTerminalSource[] } = {};
Object.values(state.steps).forEach((step) => {
if (step?.when !== undefined) {
Object.keys(step.input_connections).forEach((inputName) => {
if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) {
const terminalSource = {
name: inputName,
optional: false,
input_type: "parameter" as const,
type: "boolean" as const,
multiple: false,
label: inputName,
extensions: [],
};
if (extraInputs[step.id]) {
extraInputs[step.id]!.push(terminalSource);
} else {
extraInputs[step.id] = [terminalSource];
}
}
});
}
});
return (stepId: number) => extraInputs[stepId] || [];
return (stepId: number) => this.stepExtraInputs[stepId] || [];
},
getStepIndex(state: State) {
return Math.max(...Object.values(state.steps).map((step) => step.id), state.stepIndex);
@@ -206,18 +185,19 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
actions: {
addStep(newStep: NewStep): Step {
const stepId = newStep.id ? newStep.id : this.getStepIndex + 1;
newStep.id = stepId;
const step = newStep as Step;
const step = Object.freeze({ ...newStep, id: stepId } as Step);
Vue.set(this.steps, stepId.toString(), step);
const connectionStore = useConnectionStore();
stepToConnections(step).map((connection) => connectionStore.addConnection(connection));
this.stepExtraInputs[step.id] = getStepExtraInputs(step);
return step;
},
updateStep(this: State, step: Step) {
step.workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
const workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
step.outputs.find((output) => workflowOutput.output_name == output.name)
);
this.steps[step.id.toString()] = step;
this.steps[step.id.toString()] = Object.freeze({ ...step, workflow_outputs });
this.stepExtraInputs[step.id] = getStepExtraInputs(step);
},
changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) {
Vue.set(this.stepMapOver, stepId, mapOver);
@@ -298,6 +278,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
.getConnectionsForStep(stepId)
.forEach((connection) => connectionStore.removeConnection(connection.id));
Vue.delete(this.steps, stepId.toString());
Vue.delete(this.stepExtraInputs, stepId);
},
},
});
@@ -335,3 +316,24 @@ export function stepToConnections(step: Step): Connection[] {
}
return connections;
}
function getStepExtraInputs(step: Step) {
const extraInputs: InputTerminalSource[] = [];
if (step.when !== undefined) {
Object.keys(step.input_connections).forEach((inputName) => {
if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) {
const terminalSource = {
name: inputName,
optional: false,
input_type: "parameter" as const,
type: "boolean" as const,
multiple: false,
label: inputName,
extensions: [],
};
extraInputs.push(terminalSource);
}
});
}
return extraInputs;
}
+2 -2
View File
@@ -612,8 +612,8 @@ workflow_editor:
output_terminal: "${_} [output-name='${name}']"
input_terminal: "${_} [input-name='${name}']"
input_mapping_icon: "${_} [input-name='${name}'].multiple"
workflow_output_toggle: "${_} .callout-terminal.${name}"
workflow_output_toggle_active: "${_} .callout-terminal.${name} .mark-terminal-active"
workflow_output_toggle: "${_} [data-output-name='${name}'] .callout-terminal "
workflow_output_toggle_active: "${_} [data-output-name='${name}'] .mark-terminal-active"
selectors:
canvas_body: '#workflow-canvas'
edit_annotation: '#workflow-annotation'
+13
View File
@@ -0,0 +1,13 @@
/**
* Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
* @param object Object which contains array
* @param key Key which array is in
* @param value Value to push
*/
export function pushOrSet<T, K extends string | number | symbol>(object: { [key in K]: Array<T> }, key: K, value: T) {
if (key in object) {
object[key]!.push(value);
} else {
object[key] = [value];
}
}
+23 -9
View File
@@ -7582,6 +7582,14 @@ last-run@^1.1.0:
default-resolution "^2.0.0"
es6-weak-map "^2.0.1"
launch-editor@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7"
integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==
dependencies:
picocolors "^1.0.0"
shell-quote "^1.7.3"
lazystream@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz"
@@ -9936,6 +9944,11 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
shell-quote@^1.7.3:
version "1.8.0"
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz"
@@ -11258,10 +11271,10 @@ webpack-dev-middleware@^5.3.1:
range-parser "^1.2.1"
schema-utils "^4.0.0"
webpack-dev-server@^4.11.1:
version "4.11.1"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5"
integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==
webpack-dev-server@^4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.12.0.tgz#e2dcad4d43e486c3bac48ddbf346e77ef03c7428"
integrity sha512-XRN9YRnvOj3TQQ5w/0pR1y1xDcVnbWtNkTri46kuEbaWUPTHsWUvOyAAI7PZHLY+hsFki2kRltJjKMw7e+IiqA==
dependencies:
"@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5"
@@ -11282,6 +11295,7 @@ webpack-dev-server@^4.11.1:
html-entities "^2.3.2"
http-proxy-middleware "^2.0.3"
ipaddr.js "^2.0.1"
launch-editor "^2.6.0"
open "^8.0.9"
p-retry "^4.5.0"
rimraf "^3.0.2"
@@ -11291,7 +11305,7 @@ webpack-dev-server@^4.11.1:
sockjs "^0.3.24"
spdy "^4.0.2"
webpack-dev-middleware "^5.3.1"
ws "^8.4.2"
ws "^8.13.0"
webpack-merge@^5.7.3, webpack-merge@^5.8.0:
version "5.8.0"
@@ -11534,10 +11548,10 @@ ws@^8.11.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
ws@^8.4.2:
version "8.8.1"
resolved "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz"
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
ws@^8.13.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
xml-beautifier@^0.5.0:
version "0.5.0"
+3
View File
@@ -4881,6 +4881,9 @@
The `broker_url` option, if unset, defaults to the value of
`amqp_internal_connection`. The `result_backend` option must be
set if the `enable_celery_tasks` option is set.
The galaxy.fetch_data task can be disabled by setting its route to
"disabled": `galaxy.fetch_data: disabled`. (Other tasks cannot be
disabled on a per-task basis at this time.)
For details, see Celery documentation at
https://docs.celeryq.dev/en/stable/userguide/configuration.html.
:Default: ``{'task_routes': {'galaxy.fetch_data': 'galaxy.external', 'galaxy.set_job_metadata': 'galaxy.external'}}``
+12
View File
@@ -137,6 +137,7 @@ LOGGING_CONFIG_DEFAULT: Dict[str, Any] = {
VERSION_JSON_FILE = "version.json"
DEFAULT_EMAIL_FROM_LOCAL_PART = "galaxy-no-reply"
DISABLED_FLAG = "disabled" # Used to mark a config option as disabled
def configure_logging(config, facts=None):
@@ -1289,6 +1290,17 @@ class GalaxyAppConfiguration(BaseAppConfiguration, CommonConfigurationMixin):
f"Config option '{key}' is deprecated and will be removed in a future release. Please consult the latest version of the sample configuration file."
)
def is_fetch_with_celery_enabled(self):
"""
True iff celery is enabled and celery_conf["task_routes"]["galaxy.fetch_data"] != DISABLED_FLAG.
"""
celery_enabled = self.enable_celery_tasks
try:
fetch_disabled = self.celery_conf["task_routes"]["galaxy.fetch_data"] == DISABLED_FLAG
except (TypeError, KeyError): # celery_conf is None or sub-dictionary is none or either key is not present
fetch_disabled = False
return celery_enabled and not fetch_disabled
@staticmethod
def _parse_allowed_origin_hostnames(allowed_origin_hostnames):
"""
@@ -2613,6 +2613,9 @@ galaxy:
# The `broker_url` option, if unset, defaults to the value of
# `amqp_internal_connection`. The `result_backend` option must be set
# if the `enable_celery_tasks` option is set.
# The galaxy.fetch_data task can be disabled by setting its route to
# "disabled": `galaxy.fetch_data: disabled`. (Other tasks cannot be
# disabled on a per-task basis at this time.)
# For details, see Celery documentation at
# https://docs.celeryq.dev/en/stable/userguide/configuration.html.
#celery_conf:
@@ -3564,6 +3564,9 @@ mapping:
The `broker_url` option, if unset, defaults to the value of `amqp_internal_connection`.
The `result_backend` option must be set if the `enable_celery_tasks` option is set.
The galaxy.fetch_data task can be disabled by setting its route to "disabled": `galaxy.fetch_data: disabled`.
(Other tasks cannot be disabled on a per-task basis at this time.)
For details, see Celery documentation at https://docs.celeryq.dev/en/stable/userguide/configuration.html.
enable_celery_tasks:
@@ -187,7 +187,7 @@ tinydb==4.7.1 ; python_version >= "3.7" and python_version < "3.12"
tornado==6.2 ; python_version >= "3.7" and python_version < "3.12"
tqdm==4.64.1 ; python_version >= "3.7" and python_version < "3.12"
tuspy==1.0.0 ; python_version >= "3.7" and python_version < "3.12"
tuswsgi==0.5.4 ; python_version >= "3.7" and python_version < "3.12"
tuswsgi==0.5.5 ; python_version >= "3.7" and python_version < "3.12"
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.12"
tzlocal==2.1 ; python_version >= "3.7" and python_version < "3.12"
ubiquerg==0.6.2 ; python_version >= "3.7" and python_version < "3.12"
+1 -1
View File
@@ -182,7 +182,7 @@ def execute(
tool_id = tool.id
for job2 in execution_tracker.successful_jobs:
# Put the job in the queue if tracking in memory
if tool_id == "__DATA_FETCH__" and tool.app.config.enable_celery_tasks:
if tool_id == "__DATA_FETCH__" and tool.app.config.is_fetch_with_celery_enabled():
job_id = job2.id
from galaxy.celery.tasks import (
fetch_data,
@@ -1,6 +1,6 @@
#!/usr/bin/env python
"""
Unpack a tar or tar.gz archive into a directory.
Unpack a tar, tar.gz or zip archive into a directory.
usage: %prog archive_source dest_dir
--[url|file] source type, either a URL or a file.
@@ -11,6 +11,7 @@ import math
import optparse
import os
import tarfile
import zipfile
from base64 import b64decode
from galaxy.files import ConfiguredFileSources
@@ -35,11 +36,19 @@ def check_archive(archive_file, dest_dir):
Ensure that a tar archive has no absolute paths or relative paths outside
the archive.
"""
with tarfile.open(archive_file, mode="r") as archive_fp:
for arc_path in archive_fp.getnames():
assert os.path.normpath(os.path.join(dest_dir, arc_path)).startswith(
dest_dir.rstrip(os.sep) + os.sep
), f"Archive member would extract outside target directory: {arc_path}"
if zipfile.is_zipfile(archive_file):
with zipfile.ZipFile(archive_file, "r") as archive_fp:
for arc_path in archive_fp.namelist():
assert not os.path.isabs(arc_path), f"Archive member has absolute path: {arc_path}"
assert not os.path.relpath(arc_path).startswith(
".."
), f"Archive member would extract outside target directory: {arc_path}"
else:
with tarfile.open(archive_file, mode="r") as archive_fp:
for arc_path in archive_fp.getnames():
assert os.path.normpath(os.path.join(dest_dir, arc_path)).startswith(
dest_dir.rstrip(os.sep) + os.sep
), f"Archive member would extract outside target directory: {arc_path}"
return True
@@ -47,9 +56,13 @@ def unpack_archive(archive_file, dest_dir):
"""
Unpack a tar and/or gzipped archive into a destination directory.
"""
archive_fp = tarfile.open(archive_file, mode="r")
archive_fp.extractall(path=dest_dir)
archive_fp.close()
if zipfile.is_zipfile(archive_file):
with zipfile.ZipFile(archive_file, "r") as zip_archive:
zip_archive.extractall(path=dest_dir)
else:
archive_fp = tarfile.open(archive_file, mode="r")
archive_fp.extractall(path=dest_dir)
archive_fp.close()
def main(options, args):
+42 -1
View File
@@ -7,7 +7,7 @@ from galaxy.config import DEFAULT_EMAIL_FROM_LOCAL_PART
from galaxy.util.properties import running_from_source
@pytest.fixture(scope="module")
@pytest.fixture()
def appconfig():
return config.GalaxyAppConfiguration(override_tempdir=False)
@@ -50,3 +50,44 @@ def test_assign_email_from(monkeypatch):
override_tempdir=False, galaxy_infrastructure_url="http://myhost:8080/galaxy/"
)
assert appconfig.email_from == f"{DEFAULT_EMAIL_FROM_LOCAL_PART}@myhost"
class TestIsFetchWithCeleryEnabled:
def test_disabled_if_celery_disabled(self, appconfig):
appconfig.enable_celery_tasks = False
assert not appconfig.is_fetch_with_celery_enabled()
def test_enabled_if_no_celeryconf(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf = None
assert appconfig.is_fetch_with_celery_enabled()
def test_enabled_if_no_task_routes_key(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf = {"some-other-key": 1}
assert appconfig.is_fetch_with_celery_enabled()
def test_enabled_if_task_routes_empty(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf["task_routes"] = None
assert appconfig.is_fetch_with_celery_enabled()
def test_enabled_if_no_route_key(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf["task_routes"] = {"some-other-route": 1}
assert appconfig.is_fetch_with_celery_enabled()
def test_enabled_if_no_route(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = None
assert appconfig.is_fetch_with_celery_enabled()
def test_enabled_if_has_route(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = "my_route"
assert appconfig.is_fetch_with_celery_enabled()
def test_disabled_if_disabled_flag(self, appconfig):
appconfig.enable_celery_tasks = True
appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = config.DISABLED_FLAG
assert not appconfig.is_fetch_with_celery_enabled()