This is an automated email from the ASF dual-hosted git repository.

klesh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 98c1c4593 [feat] q-dev-plugin-collect-s3 (#8454)
98c1c4593 is described below

commit 98c1c4593f453537b644f9a634552b84e3b555a1
Author: Warren Chen <[email protected]>
AuthorDate: Wed May 28 21:57:20 2025 +0800

    [feat] q-dev-plugin-collect-s3 (#8454)
    
    * feat: new plugin = q dev
    
    * fix: tables to archived
    
    * fix: archived
    
    * fix: translate docs
    
    * fix: modify test
---
 backend/go.mod                                     |   1 +
 backend/go.sum                                     |   1 +
 backend/plugins/q_dev/Q_DEV_deploy_guide.md        |  85 ++++++++++++
 backend/plugins/q_dev/README.md                    |  97 +++++++++++++
 backend/plugins/q_dev/api/connection.go            |  96 +++++++++++++
 backend/plugins/q_dev/api/init.go                  |  39 ++++++
 backend/plugins/q_dev/api/test_connection.go       |  67 +++++++++
 backend/plugins/q_dev/img.png                      | Bin 0 -> 20568 bytes
 backend/plugins/q_dev/impl/impl.go                 | 150 +++++++++++++++++++++
 backend/plugins/q_dev/models/connection.go         |  69 ++++++++++
 .../q_dev/models/migrationscripts/20250319_init.go |  45 +++++++
 .../migrationscripts/20250320_modify_file_meta.go  |  46 +++++++
 .../models/migrationscripts/archived/connection.go |  46 +++++++
 .../migrationscripts/archived/s3_file_meta.go      |  38 ++++++
 .../models/migrationscripts/archived/user_data.go  |  51 +++++++
 .../migrationscripts/archived/user_metrics.go      |  66 +++++++++
 .../q_dev/models/migrationscripts/register.go      |  29 ++++
 backend/plugins/q_dev/models/s3_file_meta.go       |  38 ++++++
 backend/plugins/q_dev/models/user_data.go          |  51 +++++++
 backend/plugins/q_dev/models/user_metrics.go       |  66 +++++++++
 backend/plugins/q_dev/q_dev.go                     |  43 ++++++
 backend/plugins/q_dev/tasks/s3_client.go           |  47 +++++++
 backend/plugins/q_dev/tasks/s3_file_collector.go   | 103 ++++++++++++++
 backend/plugins/q_dev/tasks/task_data.go           |  45 +++++++
 backend/plugins/table_info_test.go                 |   2 +
 25 files changed, 1321 insertions(+)

diff --git a/backend/go.mod b/backend/go.mod
index 819fcfb6d..871cba963 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -3,6 +3,7 @@ module github.com/apache/incubator-devlake
 go 1.20
 
 require (
+       github.com/aws/aws-sdk-go v1.55.6
        github.com/cockroachdb/errors v1.11.1
        github.com/gin-contrib/cors v1.6.0
        github.com/gin-gonic/gin v1.9.1
diff --git a/backend/go.sum b/backend/go.sum
index 70c2daa08..a6e2f17ba 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -54,6 +54,7 @@ github.com/armon/circbuf 
v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod 
h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod 
h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 
h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/aws/aws-sdk-go v1.55.6/go.mod 
h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
 github.com/bgentry/speakeasy v0.1.0/go.mod 
h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.4/go.mod 
h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
 github.com/bwesterb/go-ristretto v1.2.3/go.mod 
h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
diff --git a/backend/plugins/q_dev/Q_DEV_deploy_guide.md 
b/backend/plugins/q_dev/Q_DEV_deploy_guide.md
new file mode 100644
index 000000000..218c55c6e
--- /dev/null
+++ b/backend/plugins/q_dev/Q_DEV_deploy_guide.md
@@ -0,0 +1,85 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+
+# DevLake Development Environment Deployment Guide
+
+## Environment Requirements
+- Docker v19.03.10+
+- Golang v1.19+
+- GNU Make
+    - Mac (pre-installed)
+    - Windows: [Download](http://gnuwin32.sourceforge.net/packages/make.htm)
+    - Ubuntu: `sudo apt-get install build-essential libssl-dev`
+
+## How to Set Up the Development Environment
+The following guide will explain how to run DevLake's frontend (config-ui) and 
backend in development mode.
+
+### Clone the Repository
+Navigate to where you want to install this project and clone the repository:
+
+```bash
+git clone https://github.com/apache/incubator-devlake.git
+cd incubator-devlake
+```
+
+### Install Plugin Dependencies
+
+RefDiff plugin:
+Install Go packages
+```bash
+cd backend
+go get
+cd ..
+```
+
+### Configure Environment File
+Copy the example configuration file to a new local file:
+
+```bash
+cp env.example .env
+```
+
+Update the following variables in the `.env` file:
+
+- `DB_URL`: Replace `mysql:3306` with `127.0.0.1:3306`
+- `DISABLED_REMOTE_PLUGINS`: Set to `True`
+
+### Start MySQL and Grafana Containers
+
+Make sure the Docker daemon is running before this step.
+
+> Grafana needs to rebuild the image, then change the image in 
docker-compose.datasources.yml to `image: grafana:latest`
+
+```bash
+docker-compose -f docker-compose-dev.yml up -d mysql grafana
+```
+
+### Run in Development Mode
+Run devlake and config-ui in development mode in two separate terminals:
+
+```bash
+# Install poetry, follow the guide: 
https://python-poetry.org/docs/#installation
+# Run devlake, only using the q dev plugin here
+DEVLAKE_PLUGINS=q_dev nohup make dev &
+# Run config-ui
+make configure-dev
+```
+
+For common errors, please refer to the troubleshooting documentation.
+
+Config UI runs on localhost:4000
\ No newline at end of file
diff --git a/backend/plugins/q_dev/README.md b/backend/plugins/q_dev/README.md
new file mode 100644
index 000000000..331359083
--- /dev/null
+++ b/backend/plugins/q_dev/README.md
@@ -0,0 +1,97 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Q Developer Plugin
+
+This plugin is used to retrieve AWS Q Developer usage data from AWS S3, and 
process and analyze it.
+
+## Features
+
+- Retrieve CSV files from a specified prefix in AWS S3
+- Parse user usage data from CSV files
+- Aggregate data by user and calculate various metrics
+
+## Configuration
+
+Configuration items include:
+
+1. AWS Access Key ID
+2. AWS Secret Key
+3. AWS Region
+4. S3 Bucket Name
+5. Rate Limit (per hour)
+
+You can create a connection using the following curl command:
+```bash
+curl 'http://localhost:8080/plugins/q_dev/connections' \
+--header 'Content-Type: application/json' \
+--data-raw '{
+    "name": "q_dev_connection",
+    "accessKeyId": "<YOUR_ACCESS_KEY_ID>",
+    "secretAccessKey": "<YOUR_SECRET_ACCESS_KEY>",
+    "region": "<AWS_REGION>",
+    "bucket": "<YOUR_S3_BUCKET_NAME>",
+    "rateLimitPerHour": 20000
+}'
+```
+Please replace the following placeholders with actual values:
+<YOUR_ACCESS_KEY_ID>: Your AWS access key ID
+<YOUR_SECRET_ACCESS_KEY>: Your AWS secret access key
+<YOUR_S3_BUCKET_NAME>: The S3 bucket name you want to use
+<AWS_REGION>: The region where your S3 bucket is located
+
+You can get all connections using the following curl command:
+```bash
+curl Get 'http://localhost:8080/plugins/q_dev/connections'
+```
+
+## Data Flow
+
+The plugin includes the following tasks:
+
+1. `collectQDevS3Files`: Collects file metadata information from S3, without 
downloading file content
+2. `extractQDevS3Data`: Uses S3 file metadata to download CSV data and parse 
it into the database
+3. `convertQDevUserMetrics`: Converts user data into aggregated metrics, 
calculating averages and totals
+
+## Data Tables
+
+- `_tool_q_dev_connections`: Stores AWS S3 connection information
+- `_tool_q_dev_s3_file_meta`: Stores S3 file metadata
+- `_tool_q_dev_user_data`: Stores user data parsed from CSV files
+- `_tool_q_dev_user_metrics`: Stores aggregated user metrics
+
+## Data Collection Configuration
+Steps to collect data:
+1. On the Config UI page, select `Advanced Mode` on the left, click 
`Blueprints`
+2. Create a new Blueprint
+3. ![img.png](img.png) Click the gear icon on the right
+4. Paste the following JSON configuration into `JSON Configuration`:
+
+```json
+[
+  [
+    {
+      "plugin": "q_dev",
+      "subtasks": null,
+      "options": {
+        "connectionId": 5,
+        "s3Prefix": ""
+      }
+    }
+  ]
+]
+```
\ No newline at end of file
diff --git a/backend/plugins/q_dev/api/connection.go 
b/backend/plugins/q_dev/api/connection.go
new file mode 100644
index 000000000..24cf0ced4
--- /dev/null
+++ b/backend/plugins/q_dev/api/connection.go
@@ -0,0 +1,96 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "net/http"
+
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/q_dev/models"
+)
+
+// 连接项目的CRUD API
+
+// PostConnections 创建新连接
+func PostConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // 创建连接
+       connection := &models.QDevConnection{}
+       err := api.Decode(input.Body, connection, vld)
+       if err != nil {
+               return nil, err
+       }
+       // 验证
+       // 保存到数据库
+       err = connectionHelper.Create(connection, input)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
+}
+
+// PatchConnection 更新现有连接
+func PatchConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.QDevConnection{}
+       if err := connectionHelper.First(&connection, input.Params); err != nil 
{
+               return nil, err
+       }
+       if err := (&models.QDevConnection{}).MergeFromRequest(connection, 
input.Body); err != nil {
+               return nil, errors.Convert(err)
+       }
+       if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != 
nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize(), Status: 
http.StatusOK}, nil
+}
+
+// DeleteConnection 删除连接
+func DeleteConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       conn := &models.QDevConnection{}
+       output, err := connectionHelper.Delete(conn, input)
+       if err != nil {
+               return output, err
+       }
+       output.Body = conn.Sanitize()
+       return output, nil
+}
+
+// ListConnections 列出所有连接
+func ListConnections(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       var connections []models.QDevConnection
+       err := connectionHelper.List(&connections)
+       if err != nil {
+               return nil, err
+       }
+       // 敏感信息脱敏
+       for i := 0; i < len(connections); i++ {
+               connections[i] = connections[i].Sanitize()
+       }
+       return &plugin.ApiResourceOutput{Body: connections}, nil
+}
+
+// GetConnection 获取单个连接详情
+func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, 
errors.Error) {
+       connection := &models.QDevConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, err
+       }
+       return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, err
+}
diff --git a/backend/plugins/q_dev/api/init.go 
b/backend/plugins/q_dev/api/init.go
new file mode 100644
index 000000000..7a16d5f0c
--- /dev/null
+++ b/backend/plugins/q_dev/api/init.go
@@ -0,0 +1,39 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/go-playground/validator/v10"
+)
+
+var vld *validator.Validate
+var connectionHelper *api.ConnectionApiHelper
+var basicRes context.BasicRes
+
+func Init(br context.BasicRes, p plugin.PluginMeta) {
+       basicRes = br
+       vld = validator.New()
+       connectionHelper = api.NewConnectionHelper(
+               basicRes,
+               vld,
+               p.Name(),
+       )
+}
diff --git a/backend/plugins/q_dev/api/test_connection.go 
b/backend/plugins/q_dev/api/test_connection.go
new file mode 100644
index 000000000..b5cf35e29
--- /dev/null
+++ b/backend/plugins/q_dev/api/test_connection.go
@@ -0,0 +1,67 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/q_dev/models"
+       "github.com/apache/incubator-devlake/plugins/q_dev/tasks"
+
+       "net/http"
+)
+
+// TestConnection 测试连接
+func TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       // 解析连接参数
+       var connection models.QDevConnection
+       err := api.Decode(input.Body, &connection, vld)
+       if err != nil {
+               return nil, err
+       }
+
+       // 测试S3连接
+       _, err = tasks.NewQDevS3Client(nil, &connection)
+       if err != nil {
+               return nil, err
+       }
+
+       // 连接成功
+       return &plugin.ApiResourceOutput{Status: http.StatusOK}, nil
+}
+
+// TestExistingConnection 测试现有连接
+func TestExistingConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
+       connection := &models.QDevConnection{}
+       err := connectionHelper.First(connection, input.Params)
+       if err != nil {
+               return nil, errors.BadInput.Wrap(err, "find connection from db")
+       }
+       if err := api.DecodeMapStruct(input.Body, connection, false); err != 
nil {
+               return nil, err
+       }
+       // 测试连接
+       _, err = tasks.NewQDevS3Client(nil, connection)
+       if err != nil {
+               return nil, err
+       }
+
+       // 连接成功
+       return &plugin.ApiResourceOutput{Status: http.StatusOK}, nil
+}
diff --git a/backend/plugins/q_dev/img.png b/backend/plugins/q_dev/img.png
new file mode 100644
index 000000000..16af8d1ad
Binary files /dev/null and b/backend/plugins/q_dev/img.png differ
diff --git a/backend/plugins/q_dev/impl/impl.go 
b/backend/plugins/q_dev/impl/impl.go
new file mode 100644
index 000000000..c8ddea5c1
--- /dev/null
+++ b/backend/plugins/q_dev/impl/impl.go
@@ -0,0 +1,150 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package impl
+
+import (
+       "fmt"
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+       "github.com/apache/incubator-devlake/plugins/q_dev/api"
+       "github.com/apache/incubator-devlake/plugins/q_dev/models"
+       
"github.com/apache/incubator-devlake/plugins/q_dev/models/migrationscripts"
+       "github.com/apache/incubator-devlake/plugins/q_dev/tasks"
+)
+
+var _ interface {
+       plugin.PluginMeta
+       plugin.PluginInit
+       plugin.PluginTask
+       plugin.PluginApi
+       plugin.PluginModel
+       plugin.PluginSource
+       plugin.PluginMigration
+       plugin.CloseablePluginTask
+} = (*QDev)(nil)
+
+type QDev struct{}
+
+func (p QDev) Init(basicRes context.BasicRes) errors.Error {
+       api.Init(basicRes, p)
+       return nil
+}
+
+func (p QDev) GetTablesInfo() []dal.Tabler {
+       return []dal.Tabler{
+               &models.QDevConnection{},
+               &models.QDevUserData{},
+               &models.QDevUserMetrics{},
+               &models.QDevS3FileMeta{},
+       }
+}
+
+func (p QDev) Description() string {
+       return "To collect and enrich data from AWS Q Developer usage metrics"
+}
+
+func (p QDev) Name() string {
+       return "q_dev"
+}
+
+func (p QDev) Connection() dal.Tabler {
+       return &models.QDevConnection{}
+}
+
+func (p QDev) Scope() plugin.ToolLayerScope {
+       return nil
+}
+
+func (p QDev) ScopeConfig() dal.Tabler {
+       return nil
+}
+
+func (p QDev) SubTaskMetas() []plugin.SubTaskMeta {
+       return []plugin.SubTaskMeta{
+               tasks.CollectQDevS3FilesMeta,
+       }
+}
+
+func (p QDev) PrepareTaskData(taskCtx plugin.TaskContext, options 
map[string]interface{}) (interface{}, errors.Error) {
+       var op tasks.QDevOptions
+       if err := helper.Decode(options, &op, nil); err != nil {
+               return nil, err
+       }
+
+       connectionHelper := helper.NewConnectionHelper(
+               taskCtx,
+               nil,
+               p.Name(),
+       )
+       connection := &models.QDevConnection{}
+       err := connectionHelper.FirstById(connection, op.ConnectionId)
+       if err != nil {
+               return nil, err
+       }
+
+       // 创建S3客户端,替代API客户端
+       s3Client, err := tasks.NewQDevS3Client(taskCtx, connection)
+       if err != nil {
+               return nil, err
+       }
+
+       return &tasks.QDevTaskData{
+               Options:  &op,
+               S3Client: s3Client,
+       }, nil
+}
+
+func (p QDev) RootPkgPath() string {
+       return "github.com/apache/incubator-devlake/plugins/q_dev"
+}
+
+func (p QDev) MigrationScripts() []plugin.MigrationScript {
+       return migrationscripts.All()
+}
+
+func (p QDev) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
+       return map[string]map[string]plugin.ApiResourceHandler{
+               "test": {
+                       "POST": api.TestConnection,
+               },
+               "connections": {
+                       "POST": api.PostConnections,
+                       "GET":  api.ListConnections,
+               },
+               "connections/:connectionId": {
+                       "PATCH":  api.PatchConnection,
+                       "DELETE": api.DeleteConnection,
+                       "GET":    api.GetConnection,
+               },
+               "connections/:connectionId/test": {
+                       "POST": api.TestExistingConnection,
+               },
+       }
+}
+
+func (p QDev) Close(taskCtx plugin.TaskContext) errors.Error {
+       data, ok := taskCtx.GetData().(*tasks.QDevTaskData)
+       if !ok {
+               return errors.Default.New(fmt.Sprintf("GetData failed when try 
to close %+v", taskCtx))
+       }
+       data.S3Client.Close()
+       return nil
+}
diff --git a/backend/plugins/q_dev/models/connection.go 
b/backend/plugins/q_dev/models/connection.go
new file mode 100644
index 000000000..5f56749a5
--- /dev/null
+++ b/backend/plugins/q_dev/models/connection.go
@@ -0,0 +1,69 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/utils"
+       helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+// QDevConn holds the essential information to connect to AWS S3
+type QDevConn struct {
+       // AccessKeyId for AWS
+       AccessKeyId string `mapstructure:"accessKeyId" json:"accessKeyId"`
+       // SecretAccessKey for AWS
+       SecretAccessKey string `mapstructure:"secretAccessKey" 
json:"secretAccessKey"`
+       // Region for AWS
+       Region string `mapstructure:"region" json:"region"`
+       // Bucket for AWS S3
+       Bucket string `mapstructure:"bucket" json:"bucket"`
+       // RateLimitPerHour limits the API requests sent to AWS
+       RateLimitPerHour int `mapstructure:"rateLimitPerHour" 
json:"rateLimitPerHour"`
+}
+
+func (conn *QDevConn) Sanitize() QDevConn {
+       conn.SecretAccessKey = utils.SanitizeString(conn.SecretAccessKey)
+       return *conn
+}
+
+// QDevConnection holds QDevConn plus ID/Name for database storage
+type QDevConnection struct {
+       helper.BaseConnection `mapstructure:",squash"`
+       QDevConn              `mapstructure:",squash"`
+}
+
+func (QDevConnection) TableName() string {
+       return "_tool_q_dev_connections"
+}
+
+func (connection QDevConnection) Sanitize() QDevConnection {
+       connection.QDevConn = connection.QDevConn.Sanitize()
+       return connection
+}
+
+func (connection *QDevConnection) MergeFromRequest(target *QDevConnection, 
body map[string]interface{}) error {
+       secretKey := target.SecretAccessKey
+       if err := helper.DecodeMapStruct(body, target, true); err != nil {
+               return err
+       }
+       modifiedSecretKey := target.SecretAccessKey
+       if modifiedSecretKey == "" || modifiedSecretKey == 
utils.SanitizeString(secretKey) {
+               target.SecretAccessKey = secretKey
+       }
+       return nil
+}
diff --git a/backend/plugins/q_dev/models/migrationscripts/20250319_init.go 
b/backend/plugins/q_dev/models/migrationscripts/20250319_init.go
new file mode 100644
index 000000000..f73c50a90
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/20250319_init.go
@@ -0,0 +1,45 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/helpers/migrationhelper"
+       
"github.com/apache/incubator-devlake/plugins/q_dev/models/migrationscripts/archived"
+)
+
+type initTables struct{}
+
+func (*initTables) Name() string {
+       return "Init schema for Q Developer plugin"
+}
+
+func (*initTables) Up(basicRes context.BasicRes) errors.Error {
+       return migrationhelper.AutoMigrateTables(
+               basicRes,
+               &archived.QDevConnection{},
+               &archived.QDevUserData{},
+               &archived.QDevUserMetrics{},
+               &archived.QDevS3FileMeta{},
+       )
+}
+
+func (*initTables) Version() uint64 {
+       return 20250319
+}
diff --git 
a/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go 
b/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go
new file mode 100644
index 000000000..a847d041e
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/20250320_modify_file_meta.go
@@ -0,0 +1,46 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/context"
+       "github.com/apache/incubator-devlake/core/errors"
+)
+
+type modifyFileMetaTable struct{}
+
+func (*modifyFileMetaTable) Name() string {
+       return "Modify QDevS3FileMeta table to allow NULL processed_time"
+}
+
+func (*modifyFileMetaTable) Up(basicRes context.BasicRes) errors.Error {
+       db := basicRes.GetDal()
+       
+       // 修改 processed_time 列允许为 NULL
+       sql := "ALTER TABLE _tool_q_dev_s3_file_meta MODIFY processed_time 
DATETIME NULL"
+       err := db.Exec(sql)
+       if err != nil {
+               return errors.Default.Wrap(err, "failed to modify 
processed_time column")
+       }
+       
+       return nil
+}
+
+func (*modifyFileMetaTable) Version() uint64 {
+       return 20250320
+}
diff --git 
a/backend/plugins/q_dev/models/migrationscripts/archived/connection.go 
b/backend/plugins/q_dev/models/migrationscripts/archived/connection.go
new file mode 100644
index 000000000..5d03159cc
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/archived/connection.go
@@ -0,0 +1,46 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       commonArchived 
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+// QDevConn holds the essential information to connect to AWS S3
+type QDevConn struct {
+       // AccessKeyId for AWS
+       AccessKeyId string `mapstructure:"accessKeyId" json:"accessKeyId"`
+       // SecretAccessKey for AWS
+       SecretAccessKey string `mapstructure:"secretAccessKey" 
json:"secretAccessKey"`
+       // Region for AWS
+       Region string `mapstructure:"region" json:"region"`
+       // Bucket for AWS S3
+       Bucket string `mapstructure:"bucket" json:"bucket"`
+       // RateLimitPerHour limits the API requests sent to AWS
+       RateLimitPerHour int `mapstructure:"rateLimitPerHour" 
json:"rateLimitPerHour"`
+}
+
+// QDevConnection holds QDevConn plus ID/Name for database storage
+type QDevConnection struct {
+       commonArchived.BaseConnection `mapstructure:",squash"`
+       QDevConn                      `mapstructure:",squash"`
+}
+
+func (QDevConnection) TableName() string {
+       return "_tool_q_dev_connections"
+}
diff --git 
a/backend/plugins/q_dev/models/migrationscripts/archived/s3_file_meta.go 
b/backend/plugins/q_dev/models/migrationscripts/archived/s3_file_meta.go
new file mode 100644
index 000000000..a648563a5
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/archived/s3_file_meta.go
@@ -0,0 +1,38 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       "time"
+
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+// QDevS3FileMeta 存储S3文件的元数据信息
+type QDevS3FileMeta struct {
+       archived.NoPKModel
+       ConnectionId  uint64     `gorm:"primaryKey"`
+       FileName      string     `gorm:"primaryKey;type:varchar(255)"`
+       S3Path        string     `gorm:"type:varchar(512)" json:"s3Path"`
+       Processed     bool       `gorm:"default:false"`
+       ProcessedTime *time.Time `gorm:"default:null"`
+}
+
+func (QDevS3FileMeta) TableName() string {
+       return "_tool_q_dev_s3_file_meta"
+}
diff --git 
a/backend/plugins/q_dev/models/migrationscripts/archived/user_data.go 
b/backend/plugins/q_dev/models/migrationscripts/archived/user_data.go
new file mode 100644
index 000000000..00e6a0a44
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/archived/user_data.go
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       "time"
+
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+)
+
+// QDevUserData 存储从CSV中提取的原始数据
+type QDevUserData struct {
+       archived.Model
+       ConnectionId                      uint64    `gorm:"primaryKey"`
+       UserId                            string    `gorm:"index" json:"userId"`
+       Date                              time.Time `gorm:"index" json:"date"`
+       CodeReview_FindingsCount          int
+       CodeReview_SucceededEventCount    int
+       InlineChat_AcceptanceEventCount   int
+       InlineChat_AcceptedLineAdditions  int
+       InlineChat_AcceptedLineDeletions  int
+       InlineChat_DismissalEventCount    int
+       InlineChat_DismissedLineAdditions int
+       InlineChat_DismissedLineDeletions int
+       InlineChat_RejectedLineAdditions  int
+       InlineChat_RejectedLineDeletions  int
+       InlineChat_RejectionEventCount    int
+       InlineChat_TotalEventCount        int
+       Inline_AICodeLines                int
+       Inline_AcceptanceCount            int
+       Inline_SuggestionsCount           int
+}
+
+func (QDevUserData) TableName() string {
+       return "_tool_q_dev_user_data"
+}
diff --git 
a/backend/plugins/q_dev/models/migrationscripts/archived/user_metrics.go 
b/backend/plugins/q_dev/models/migrationscripts/archived/user_metrics.go
new file mode 100644
index 000000000..d26b75aea
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/archived/user_metrics.go
@@ -0,0 +1,66 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package archived
+
+import (
+       
"github.com/apache/incubator-devlake/core/models/migrationscripts/archived"
+       "time"
+)
+
+// QDevUserMetrics 存储按用户聚合的指标数据
+type QDevUserMetrics struct {
+       archived.NoPKModel
+       ConnectionId uint64 `gorm:"primaryKey"`
+       UserId       string `gorm:"primaryKey"`
+       FirstDate    time.Time
+       LastDate     time.Time
+       TotalDays    int
+
+       // 聚合指标
+       TotalCodeReview_FindingsCount          int
+       TotalCodeReview_SucceededEventCount    int
+       TotalInlineChat_AcceptanceEventCount   int
+       TotalInlineChat_AcceptedLineAdditions  int
+       TotalInlineChat_AcceptedLineDeletions  int
+       TotalInlineChat_DismissalEventCount    int
+       TotalInlineChat_DismissedLineAdditions int
+       TotalInlineChat_DismissedLineDeletions int
+       TotalInlineChat_RejectedLineAdditions  int
+       TotalInlineChat_RejectedLineDeletions  int
+       TotalInlineChat_RejectionEventCount    int
+       TotalInlineChat_TotalEventCount        int
+       TotalInline_AICodeLines                int
+       TotalInline_AcceptanceCount            int
+       TotalInline_SuggestionsCount           int
+
+       // 平均指标
+       AvgCodeReview_FindingsCount        float64
+       AvgCodeReview_SucceededEventCount  float64
+       AvgInlineChat_AcceptanceEventCount float64
+       AvgInlineChat_TotalEventCount      float64
+       AvgInline_AICodeLines              float64
+       AvgInline_AcceptanceCount          float64
+       AvgInline_SuggestionsCount         float64
+
+       // 接受率指标
+       AcceptanceRate float64
+}
+
+func (QDevUserMetrics) TableName() string {
+       return "_tool_q_dev_user_metrics"
+}
diff --git a/backend/plugins/q_dev/models/migrationscripts/register.go 
b/backend/plugins/q_dev/models/migrationscripts/register.go
new file mode 100644
index 000000000..885bdf2fc
--- /dev/null
+++ b/backend/plugins/q_dev/models/migrationscripts/register.go
@@ -0,0 +1,29 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+       "github.com/apache/incubator-devlake/core/plugin"
+)
+
+// All return all migration scripts
+func All() []plugin.MigrationScript {
+       return []plugin.MigrationScript{
+               new(initTables),
+       }
+} 
\ No newline at end of file
diff --git a/backend/plugins/q_dev/models/s3_file_meta.go 
b/backend/plugins/q_dev/models/s3_file_meta.go
new file mode 100644
index 000000000..003cbc16a
--- /dev/null
+++ b/backend/plugins/q_dev/models/s3_file_meta.go
@@ -0,0 +1,38 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "time"
+
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// QDevS3FileMeta 存储S3文件的元数据信息
+type QDevS3FileMeta struct {
+       common.NoPKModel
+       ConnectionId  uint64     `gorm:"primaryKey"`
+       FileName      string     `gorm:"primaryKey;type:varchar(255)"`
+       S3Path        string     `gorm:"type:varchar(512)" json:"s3Path"`
+       Processed     bool       `gorm:"default:false"`
+       ProcessedTime *time.Time `gorm:"default:null"`
+}
+
+func (QDevS3FileMeta) TableName() string {
+       return "_tool_q_dev_s3_file_meta"
+}
diff --git a/backend/plugins/q_dev/models/user_data.go 
b/backend/plugins/q_dev/models/user_data.go
new file mode 100644
index 000000000..ce0692b9b
--- /dev/null
+++ b/backend/plugins/q_dev/models/user_data.go
@@ -0,0 +1,51 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "time"
+
+       "github.com/apache/incubator-devlake/core/models/common"
+)
+
+// QDevUserData 存储从CSV中提取的原始数据
+type QDevUserData struct {
+       common.Model
+       ConnectionId                      uint64    `gorm:"primaryKey"`
+       UserId                            string    `gorm:"index" json:"userId"`
+       Date                              time.Time `gorm:"index" json:"date"`
+       CodeReview_FindingsCount          int
+       CodeReview_SucceededEventCount    int
+       InlineChat_AcceptanceEventCount   int
+       InlineChat_AcceptedLineAdditions  int
+       InlineChat_AcceptedLineDeletions  int
+       InlineChat_DismissalEventCount    int
+       InlineChat_DismissedLineAdditions int
+       InlineChat_DismissedLineDeletions int
+       InlineChat_RejectedLineAdditions  int
+       InlineChat_RejectedLineDeletions  int
+       InlineChat_RejectionEventCount    int
+       InlineChat_TotalEventCount        int
+       Inline_AICodeLines                int
+       Inline_AcceptanceCount            int
+       Inline_SuggestionsCount           int
+}
+
+func (QDevUserData) TableName() string {
+       return "_tool_q_dev_user_data"
+}
diff --git a/backend/plugins/q_dev/models/user_metrics.go 
b/backend/plugins/q_dev/models/user_metrics.go
new file mode 100644
index 000000000..baebc3ead
--- /dev/null
+++ b/backend/plugins/q_dev/models/user_metrics.go
@@ -0,0 +1,66 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package models
+
+import (
+       "github.com/apache/incubator-devlake/core/models/common"
+       "time"
+)
+
+// QDevUserMetrics 存储按用户聚合的指标数据
+type QDevUserMetrics struct {
+       common.NoPKModel
+       ConnectionId                     uint64    `gorm:"primaryKey"`
+       UserId                           string    `gorm:"primaryKey"`
+       FirstDate                        time.Time
+       LastDate                         time.Time
+       TotalDays                        int
+       
+       // 聚合指标
+       TotalCodeReview_FindingsCount         int
+       TotalCodeReview_SucceededEventCount   int
+       TotalInlineChat_AcceptanceEventCount  int
+       TotalInlineChat_AcceptedLineAdditions int
+       TotalInlineChat_AcceptedLineDeletions int
+       TotalInlineChat_DismissalEventCount   int
+       TotalInlineChat_DismissedLineAdditions int
+       TotalInlineChat_DismissedLineDeletions int
+       TotalInlineChat_RejectedLineAdditions  int
+       TotalInlineChat_RejectedLineDeletions  int
+       TotalInlineChat_RejectionEventCount    int
+       TotalInlineChat_TotalEventCount        int
+       TotalInline_AICodeLines                int
+       TotalInline_AcceptanceCount            int
+       TotalInline_SuggestionsCount           int
+       
+       // 平均指标
+       AvgCodeReview_FindingsCount         float64
+       AvgCodeReview_SucceededEventCount   float64
+       AvgInlineChat_AcceptanceEventCount  float64
+       AvgInlineChat_TotalEventCount       float64
+       AvgInline_AICodeLines               float64
+       AvgInline_AcceptanceCount           float64
+       AvgInline_SuggestionsCount          float64
+       
+       // 接受率指标
+       AcceptanceRate                      float64
+}
+
+func (QDevUserMetrics) TableName() string {
+       return "_tool_q_dev_user_metrics"
+} 
\ No newline at end of file
diff --git a/backend/plugins/q_dev/q_dev.go b/backend/plugins/q_dev/q_dev.go
new file mode 100644
index 000000000..2cb9a7eea
--- /dev/null
+++ b/backend/plugins/q_dev/q_dev.go
@@ -0,0 +1,43 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+       "github.com/apache/incubator-devlake/core/runner"
+       "github.com/apache/incubator-devlake/plugins/q_dev/impl"
+       "github.com/spf13/cobra"
+)
+
+var PluginEntry impl.QDev
+
+// standalone mode for debugging
+func main() {
+       cmd := &cobra.Command{Use: "q_dev"}
+       connectionId := cmd.Flags().Uint64P("connectionId", "c", 0, "q_dev 
connection id")
+       s3Prefix := cmd.Flags().StringP("s3Prefix", "p", "", "s3 bucket prefix 
for q_dev data")
+       timeAfter := cmd.Flags().StringP("timeAfter", "a", "", "collect data 
that are created after specified time, ie 2006-01-02T15:04:05Z")
+
+       _ = cmd.MarkFlagRequired("connectionId")
+       cmd.Run = func(cmd *cobra.Command, args []string) {
+               runner.DirectRun(cmd, args, PluginEntry, map[string]interface{}{
+                       "connectionId": *connectionId,
+                       "s3Prefix":     *s3Prefix,
+               }, *timeAfter)
+       }
+       runner.RunCmd(cmd)
+} 
\ No newline at end of file
diff --git a/backend/plugins/q_dev/tasks/s3_client.go 
b/backend/plugins/q_dev/tasks/s3_client.go
new file mode 100644
index 000000000..679b99f11
--- /dev/null
+++ b/backend/plugins/q_dev/tasks/s3_client.go
@@ -0,0 +1,47 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/plugins/q_dev/models"
+       "github.com/aws/aws-sdk-go/aws"
+       "github.com/aws/aws-sdk-go/aws/credentials"
+       "github.com/aws/aws-sdk-go/aws/session"
+       "github.com/aws/aws-sdk-go/service/s3"
+)
+
+func NewQDevS3Client(taskCtx plugin.TaskContext, connection 
*models.QDevConnection) (*QDevS3Client, errors.Error) {
+       // 创建AWS session
+       sess, err := session.NewSession(&aws.Config{
+               Region:      aws.String(connection.Region),
+               Credentials: 
credentials.NewStaticCredentials(connection.AccessKeyId, 
connection.SecretAccessKey, ""),
+       })
+       if err != nil {
+               return nil, errors.Convert(err)
+       }
+
+       // 创建S3服务客户端
+       s3Client := s3.New(sess)
+       
+       return &QDevS3Client{
+               S3:     s3Client,
+               Bucket: connection.Bucket,
+       }, nil
+} 
\ No newline at end of file
diff --git a/backend/plugins/q_dev/tasks/s3_file_collector.go 
b/backend/plugins/q_dev/tasks/s3_file_collector.go
new file mode 100644
index 000000000..d06f066e1
--- /dev/null
+++ b/backend/plugins/q_dev/tasks/s3_file_collector.go
@@ -0,0 +1,103 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/plugin"
+       "github.com/apache/incubator-devlake/plugins/q_dev/models"
+       "github.com/aws/aws-sdk-go/aws"
+       "github.com/aws/aws-sdk-go/service/s3"
+       "strings"
+)
+
+var _ plugin.SubTaskEntryPoint = CollectQDevS3Files
+
+// CollectQDevS3Files 收集S3文件元数据
+func CollectQDevS3Files(taskCtx plugin.SubTaskContext) errors.Error {
+       data := taskCtx.GetData().(*QDevTaskData)
+       db := taskCtx.GetDal()
+
+       // 列出指定前缀下的所有对象
+       var continuationToken *string
+       prefix := data.Options.S3Prefix
+       if prefix != "" && !strings.HasSuffix(prefix, "/") {
+               prefix = prefix + "/"
+       }
+
+       taskCtx.SetProgress(0, -1)
+
+       // 清空以前的元数据记录
+       err := db.Delete(&models.QDevS3FileMeta{}, dal.Where("connection_id = 
?", data.Options.ConnectionId))
+       if err != nil {
+               return errors.Default.Wrap(err, "failed to clean previous file 
metadata")
+       }
+
+       for {
+               input := &s3.ListObjectsV2Input{
+                       Bucket:            aws.String(data.S3Client.Bucket),
+                       Prefix:            aws.String(prefix),
+                       ContinuationToken: continuationToken,
+               }
+
+               result, err := data.S3Client.S3.ListObjectsV2(input)
+               if err != nil {
+                       return errors.Convert(err)
+               }
+
+               // 处理每个CSV文件
+               for _, object := range result.Contents {
+                       // 只处理CSV文件
+                       if !strings.HasSuffix(*object.Key, ".csv") {
+                               continue
+                       }
+
+                       // 保存文件元数据
+                       fileMeta := &models.QDevS3FileMeta{
+                               ConnectionId: data.Options.ConnectionId,
+                               FileName:     *object.Key,
+                               S3Path:       *object.Key,
+                               Processed:    false,
+                       }
+
+                       err = db.Create(fileMeta)
+                       if err != nil {
+                               return errors.Default.Wrap(err, "failed to 
create file metadata")
+                       }
+
+                       taskCtx.IncProgress(1)
+               }
+
+               // 如果没有更多对象,退出循环
+               if !*result.IsTruncated {
+                       break
+               }
+
+               continuationToken = result.NextContinuationToken
+       }
+
+       return nil
+}
+
+var CollectQDevS3FilesMeta = plugin.SubTaskMeta{
+       Name:             "collectQDevS3Files",
+       EntryPoint:       CollectQDevS3Files,
+       EnabledByDefault: true,
+       Description:      "Collect S3 file metadata from AWS S3 bucket",
+}
diff --git a/backend/plugins/q_dev/tasks/task_data.go 
b/backend/plugins/q_dev/tasks/task_data.go
new file mode 100644
index 000000000..ce94a70ea
--- /dev/null
+++ b/backend/plugins/q_dev/tasks/task_data.go
@@ -0,0 +1,45 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+       "github.com/aws/aws-sdk-go/service/s3"
+)
+
+type QDevApiParams struct {
+       ConnectionId uint64 `json:"connectionId"`
+}
+
+type QDevOptions struct {
+       ConnectionId uint64 `json:"connectionId"`
+       S3Prefix     string `json:"s3Prefix"`
+}
+
+type QDevTaskData struct {
+       Options  *QDevOptions
+       S3Client *QDevS3Client
+}
+
+type QDevS3Client struct {
+       S3     *s3.S3
+       Bucket string
+}
+
+func (client *QDevS3Client) Close() {
+       // S3客户端不需要特别关闭操作
+} 
\ No newline at end of file
diff --git a/backend/plugins/table_info_test.go 
b/backend/plugins/table_info_test.go
index 83635fd58..ba83acd77 100644
--- a/backend/plugins/table_info_test.go
+++ b/backend/plugins/table_info_test.go
@@ -44,6 +44,7 @@ import (
        opsgenie "github.com/apache/incubator-devlake/plugins/opsgenie/impl"
        org "github.com/apache/incubator-devlake/plugins/org/impl"
        pagerduty "github.com/apache/incubator-devlake/plugins/pagerduty/impl"
+       q_dev "github.com/apache/incubator-devlake/plugins/q_dev/impl"
        refdiff "github.com/apache/incubator-devlake/plugins/refdiff/impl"
        slack "github.com/apache/incubator-devlake/plugins/slack/impl"
        sonarqube "github.com/apache/incubator-devlake/plugins/sonarqube/impl"
@@ -92,6 +93,7 @@ func Test_GetPluginTablesInfo(t *testing.T) {
        checker.FeedIn("opsgenie/models", opsgenie.Opsgenie{}.GetTablesInfo)
        checker.FeedIn("linker/models", linker.Linker{}.GetTablesInfo)
        checker.FeedIn("issue_trace/models", 
issueTrace.IssueTrace{}.GetTablesInfo)
+       checker.FeedIn("q_dev/models", q_dev.QDev{}.GetTablesInfo)
        err := checker.Verify()
        if err != nil {
                t.Error(err)

Reply via email to